@aiquants/daily-report 0.4.2 → 0.5.0
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/README.md +59 -0
- package/dist/client.d.mts +90 -5
- package/dist/client.d.ts +90 -5
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +4 -4
- package/dist/client.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +13 -1
- package/dist/server.d.ts +13 -1
- package/dist/server.js +6 -6
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +6 -6
- package/dist/server.mjs.map +1 -1
- package/dist/{sse-schema-BnQQh_Cc.d.ts → sse-schema-ChCypDPj.d.ts} +34 -1
- package/dist/{sse-schema-DRNjDRDy.d.mts → sse-schema-Cs1oC2DP.d.mts} +34 -1
- package/dist/styles/daily-report.standalone.css +1 -1
- package/dist/{types-DAyE_3R1.d.mts → types-BNhYC2j9.d.mts} +1 -1
- package/dist/{types-DAyE_3R1.d.ts → types-BNhYC2j9.d.ts} +1 -1
- package/package.json +1 -1
- package/src/client/components/daily-report-detail-list.tsx +62 -50
- package/src/client/components/daily-report-list.tsx +15 -3
- package/src/client/components/daily-report-resolved-content.tsx +33 -7
- package/src/client/config-context.tsx +21 -8
- package/src/client/contexts/daily-report-action-context.tsx +53 -1
- package/src/client/hooks/use-daily-report.ts +23 -1
- package/src/server/cache.spec.ts +47 -0
- package/src/server/cache.ts +27 -0
- package/src/server/handlers.ts +4 -0
- package/src/server/service.ts +18 -1
- package/src/shared/sse-schema.ts +1 -0
- package/src/shared/types.ts +1 -1
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* 日報の楽観的 CRUD と SSE 同期を提供するアクションコンテキスト。
|
|
4
4
|
*/
|
|
5
5
|
import { type Context, createContext, type ReactNode, useCallback, useContext, useEffect, useRef, useState } from "react"
|
|
6
|
+
import { useRevalidator } from "react-router"
|
|
6
7
|
import { createLogger, LogLevel } from "../../shared/logger"
|
|
7
8
|
import type { DailyReportSseMessage } from "../../shared/sse-schema"
|
|
8
9
|
import type { DailyReportCommentItem, DailyReportDetail, DailyReportItem, DailyReportUser } from "../../shared/types"
|
|
9
10
|
import { useDailyReportConfig } from "../config-context"
|
|
10
|
-
import { acquireMutationLock, applyDailyReportServerUpdates, deleteDailyReportCache, getCachedReport, registerRecentDeletion, releaseMutationLock, subscribeCacheReady, updateDailyReportCache, writeCache } from "../hooks/use-daily-report"
|
|
11
|
+
import { acquireMutationLock, applyDailyReportServerUpdates, clearDailyReportCache, DailyReportCache, deleteDailyReportCache, getCachedReport, registerRecentDeletion, releaseMutationLock, subscribeCacheReady, updateDailyReportCache, writeCache } from "../hooks/use-daily-report"
|
|
11
12
|
import { useDailyReportSseConnection } from "../hooks/use-daily-report-sse-connection"
|
|
12
13
|
|
|
13
14
|
const logger = createLogger(LogLevel.INFO, "daily-report-action-context")
|
|
@@ -80,6 +81,10 @@ type DailyReportActionContextType = {
|
|
|
80
81
|
deleteReport: (reportHubId: number, businessDate: string) => Promise<void>
|
|
81
82
|
/** stale アイテムをリストから除去する安全弁 */
|
|
82
83
|
removeStaleItem: (reportHubId: number) => void
|
|
84
|
+
/** データのみリロード (ページリロードなし) */
|
|
85
|
+
refetchData: () => Promise<void>
|
|
86
|
+
/** キャッシュを破棄してデータのみ再読み込み (ページリロードなし) */
|
|
87
|
+
clearCacheAndRefetch: () => Promise<void>
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
// HMR / dual-bundle (ESM+CJS) でもコンテキスト identity を一意に保つため、
|
|
@@ -132,6 +137,50 @@ export const DailyReportActionProvider = ({ children, user, initialItems = [], u
|
|
|
132
137
|
/** 自分が発行したアクションのID(SSEエコー無視用) */
|
|
133
138
|
const processedClientTempIds = useRef<Set<string>>(new Set())
|
|
134
139
|
|
|
140
|
+
const revalidator = useRevalidator()
|
|
141
|
+
|
|
142
|
+
const refetchData = useCallback(async () => {
|
|
143
|
+
logger.info("[DailyReportActionProvider] Refetching data without page reload...")
|
|
144
|
+
if (revalidator.state === "idle") {
|
|
145
|
+
revalidator.revalidate()
|
|
146
|
+
}
|
|
147
|
+
setVersion((v) => v + 1)
|
|
148
|
+
DailyReportCache.listeners.forEach((listener: (id: number) => void) => {
|
|
149
|
+
listener(-1)
|
|
150
|
+
})
|
|
151
|
+
}, [revalidator])
|
|
152
|
+
|
|
153
|
+
const clearCacheAndRefetch = useCallback(async () => {
|
|
154
|
+
logger.info("[DailyReportActionProvider] Clearing client & server/DB cache and refetching data without page reload...")
|
|
155
|
+
// 1. クライアント側キャッシュ破棄
|
|
156
|
+
clearDailyReportCache()
|
|
157
|
+
removedIds.current.clear()
|
|
158
|
+
try {
|
|
159
|
+
sessionStorage.clear()
|
|
160
|
+
} catch {}
|
|
161
|
+
|
|
162
|
+
// 2. サーバー/DB側キャッシュ破棄 API 呼び出し
|
|
163
|
+
try {
|
|
164
|
+
const formData = new FormData()
|
|
165
|
+
formData.append("intent", "clearCache")
|
|
166
|
+
await fetch(`${apiBasePath}/action`, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
body: formData,
|
|
169
|
+
})
|
|
170
|
+
} catch (e) {
|
|
171
|
+
logger.warn("[DailyReportActionProvider] Failed to clear server-side cache:", e)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 3. ルートローダーの再フェッチ & UI 再描画
|
|
175
|
+
if (revalidator.state === "idle") {
|
|
176
|
+
revalidator.revalidate()
|
|
177
|
+
}
|
|
178
|
+
setVersion((v) => v + 1)
|
|
179
|
+
DailyReportCache.listeners.forEach((listener: (id: number) => void) => {
|
|
180
|
+
listener(-1)
|
|
181
|
+
})
|
|
182
|
+
}, [apiBasePath, revalidator])
|
|
183
|
+
|
|
135
184
|
/**
|
|
136
185
|
* Queue for SSE messages that arrived before the target report was cached.
|
|
137
186
|
* キャッシュ未取得時に到着した SSE メッセージの待機キュー。
|
|
@@ -703,6 +752,7 @@ export const DailyReportActionProvider = ({ children, user, initialItems = [], u
|
|
|
703
752
|
createdAt: now,
|
|
704
753
|
author: getUserDisplayName(user),
|
|
705
754
|
userId: user?.id ?? "unknown",
|
|
755
|
+
sourceType: "Internal",
|
|
706
756
|
employeeName: getUserDisplayName(user),
|
|
707
757
|
updatedBy: getUserDisplayName(user),
|
|
708
758
|
updatedAt: now,
|
|
@@ -903,6 +953,8 @@ export const DailyReportActionProvider = ({ children, user, initialItems = [], u
|
|
|
903
953
|
publishReport,
|
|
904
954
|
deleteReport,
|
|
905
955
|
removeStaleItem,
|
|
956
|
+
refetchData,
|
|
957
|
+
clearCacheAndRefetch,
|
|
906
958
|
}}>
|
|
907
959
|
{children}
|
|
908
960
|
</DailyReportActionContext.Provider>
|
|
@@ -27,7 +27,7 @@ const FETCH_DEBOUNCE_MS = 120
|
|
|
27
27
|
// --- Cache Store (Centralized State) ---
|
|
28
28
|
type CacheEntry<T> = { data: T; expiresAt: number }
|
|
29
29
|
|
|
30
|
-
const DailyReportCache = {
|
|
30
|
+
export const DailyReportCache = {
|
|
31
31
|
reports: new Map<number, CacheEntry<DailyReportDetail>>(),
|
|
32
32
|
lists: new Map<string, CacheEntry<DailyReportDetail[]>>(),
|
|
33
33
|
pendingLists: new Map<string, Promise<DailyReportDetail[]>>(),
|
|
@@ -58,6 +58,23 @@ const DailyReportCache = {
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Clear all cached entries and pending requests.
|
|
63
|
+
* すべてのキャッシュエントリおよび待機中リクエストをクリアする。
|
|
64
|
+
*/
|
|
65
|
+
clear() {
|
|
66
|
+
this.reports.clear()
|
|
67
|
+
this.lists.clear()
|
|
68
|
+
this.pendingLists.clear()
|
|
69
|
+
this.pendingReports.clear()
|
|
70
|
+
this.locks.clear()
|
|
71
|
+
this.lastMutations.clear()
|
|
72
|
+
this.recentDeletes.clear()
|
|
73
|
+
this.listeners.forEach((listener) => {
|
|
74
|
+
listener(-1)
|
|
75
|
+
})
|
|
76
|
+
},
|
|
77
|
+
|
|
61
78
|
/**
|
|
62
79
|
* Check if the cache entry is still valid.
|
|
63
80
|
* キャッシュエントリがまだ有効かを確認する。
|
|
@@ -218,6 +235,11 @@ export const updateDailyReportCache = (id: number, u: Partial<DailyReportDetail>
|
|
|
218
235
|
* キャッシュからレポートを削除する。
|
|
219
236
|
*/
|
|
220
237
|
export const deleteDailyReportCache = (id: number, d: string) => DailyReportCache.delete(id, d)
|
|
238
|
+
/**
|
|
239
|
+
* Clear all daily report cache store entries.
|
|
240
|
+
* 日報の全キャッシュストアを消去する。
|
|
241
|
+
*/
|
|
242
|
+
export const clearDailyReportCache = () => DailyReportCache.clear()
|
|
221
243
|
|
|
222
244
|
export const acquireMutationLock = (id: number) => {
|
|
223
245
|
DailyReportCache.locks.add(id)
|
|
@@ -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
|
+
})
|
package/src/server/cache.ts
CHANGED
|
@@ -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
|
})()
|
|
@@ -146,6 +150,15 @@ export class SqlResultCache {
|
|
|
146
150
|
this.inFlight.delete(cacheKey)
|
|
147
151
|
}
|
|
148
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Clears all cached buckets and in-flight requests.
|
|
155
|
+
* すべてのキャッシュバケットと進行中のリクエストを全クリアします。
|
|
156
|
+
*/
|
|
157
|
+
clearAll = (): void => {
|
|
158
|
+
this.buckets.clear()
|
|
159
|
+
this.inFlight.clear()
|
|
160
|
+
}
|
|
161
|
+
|
|
149
162
|
/**
|
|
150
163
|
* Invalidates cache buckets matching a prefix.
|
|
151
164
|
* 指定されたプレフィックスに一致するキャッシュバケットを無効化します。
|
|
@@ -162,4 +175,18 @@ export class SqlResultCache {
|
|
|
162
175
|
}
|
|
163
176
|
}
|
|
164
177
|
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Cleans up all expired cache buckets to prevent memory accumulation.
|
|
181
|
+
* メモリー蓄積を防ぐため、期限切れのキャッシュバケットをすべてクリーンアップする処理。
|
|
182
|
+
*/
|
|
183
|
+
private cleanExpired(): void {
|
|
184
|
+
const now = Date.now()
|
|
185
|
+
// 期限切れのキーを削除
|
|
186
|
+
for (const [key, bucket] of this.buckets.entries()) {
|
|
187
|
+
if (bucket.expireAt <= now) {
|
|
188
|
+
this.buckets.delete(key)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
165
192
|
}
|
package/src/server/handlers.ts
CHANGED
|
@@ -317,6 +317,10 @@ export function createDailyReportHandlers(config: DailyReportHandlersConfig) {
|
|
|
317
317
|
throw e
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
|
+
case "clearCache": {
|
|
321
|
+
await service.clearCache()
|
|
322
|
+
return jsonData({ status: "OK", intent: "clearCache" })
|
|
323
|
+
}
|
|
320
324
|
default:
|
|
321
325
|
return jsonData({ error: "Invalid intent" }, { status: 400 })
|
|
322
326
|
}
|
package/src/server/service.ts
CHANGED
|
@@ -326,6 +326,7 @@ export function createDailyReportService(config: DailyReportServiceConfig) {
|
|
|
326
326
|
date: formatDateValue(hub.businessDate, "YYYY-MM-DD"),
|
|
327
327
|
author: row.creatorName ?? maskAuditActor(hub.createdBy) ?? "",
|
|
328
328
|
userId: hub.userId ? encodeUserId(hub.userId) : "",
|
|
329
|
+
sourceType: hub.sourceType ?? "Internal",
|
|
329
330
|
createdAt: formatDateValue(hub.createdAt, "YYYY-MM-DD HH:mm:ss"),
|
|
330
331
|
updatedAt: formatDateValue(hub.updatedAt, "YYYY-MM-DD HH:mm:ss"),
|
|
331
332
|
updatedBy: maskAuditActor(hub.updatedBy),
|
|
@@ -351,7 +352,6 @@ export function createDailyReportService(config: DailyReportServiceConfig) {
|
|
|
351
352
|
userId: encodeUserId(c.userId),
|
|
352
353
|
isMine: currentUserId ? c.userId === currentUserId : false,
|
|
353
354
|
})),
|
|
354
|
-
sourceType: hub.sourceType,
|
|
355
355
|
}
|
|
356
356
|
}
|
|
357
357
|
|
|
@@ -1036,6 +1036,7 @@ export function createDailyReportService(config: DailyReportServiceConfig) {
|
|
|
1036
1036
|
createdAt: formatDateValue(hub.createdAt, "YYYY-MM-DD HH:mm:ss"),
|
|
1037
1037
|
author: userName,
|
|
1038
1038
|
userId: encodeUserId(userId),
|
|
1039
|
+
sourceType: "Internal",
|
|
1039
1040
|
employeeName: userName,
|
|
1040
1041
|
// 監査列と同様、クライアント公開時は生内部 ID を難読化する (mapHubRecord と整合)
|
|
1041
1042
|
updatedBy: encodeUserId(userId),
|
|
@@ -1279,10 +1280,26 @@ export function createDailyReportService(config: DailyReportServiceConfig) {
|
|
|
1279
1280
|
return fullDetail ?? null
|
|
1280
1281
|
}
|
|
1281
1282
|
|
|
1283
|
+
/**
|
|
1284
|
+
* Clears all server-side SQL result and user ID caches.
|
|
1285
|
+
* サーバー側のすべての SQL 結果キャッシュおよびユーザー ID キャッシュを全消去する。
|
|
1286
|
+
*/
|
|
1287
|
+
const clearCache = async (): Promise<void> => {
|
|
1288
|
+
sqlResultCache.clearAll()
|
|
1289
|
+
userIdCache.clear()
|
|
1290
|
+
cachedDraftLabelId = null
|
|
1291
|
+
if (epochs) {
|
|
1292
|
+
await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)
|
|
1293
|
+
}
|
|
1294
|
+
logger.info("[DailyReportService] Server-side DB/SQL caches cleared successfully.")
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1282
1297
|
return {
|
|
1283
1298
|
// 定数 (アプリ側の互換 export 用)
|
|
1284
1299
|
streamKey: DAILY_REPORT_SSE_STREAM_KEY,
|
|
1285
1300
|
streamMaxLen: DAILY_REPORT_SSE_STREAM_MAXLEN,
|
|
1301
|
+
// キャッシュクリア
|
|
1302
|
+
clearCache,
|
|
1286
1303
|
// ユーザー解決
|
|
1287
1304
|
getUserIdByExternalId,
|
|
1288
1305
|
// 参照系
|
package/src/shared/sse-schema.ts
CHANGED
|
@@ -39,6 +39,7 @@ export const dailyReportDetailSchema = z.object({
|
|
|
39
39
|
createdAt: z.string().nullable(),
|
|
40
40
|
author: z.string(),
|
|
41
41
|
userId: z.string(),
|
|
42
|
+
sourceType: z.string().nullable(),
|
|
42
43
|
employeeName: z.string().nullable(),
|
|
43
44
|
updatedBy: z.string().nullable(),
|
|
44
45
|
updatedAt: z.string().nullable(),
|
package/src/shared/types.ts
CHANGED
|
@@ -47,6 +47,7 @@ export type DailyReportDetail = {
|
|
|
47
47
|
createdAt: string | null
|
|
48
48
|
author: string
|
|
49
49
|
userId: string
|
|
50
|
+
sourceType: string | null
|
|
50
51
|
employeeName: string | null
|
|
51
52
|
updatedBy: string | null
|
|
52
53
|
updatedAt: string | null
|
|
@@ -63,7 +64,6 @@ export type DailyReportDetail = {
|
|
|
63
64
|
isStarred: boolean
|
|
64
65
|
labels: DailyReportLabelDef[]
|
|
65
66
|
commentItems: DailyReportCommentItem[]
|
|
66
|
-
sourceType?: string
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|