@aiquants/daily-report 0.1.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/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/client.d.mts +449 -0
- package/dist/client.d.ts +449 -0
- package/dist/client.js +7 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +7 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +42 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger-D3krZrNK.d.mts +29 -0
- package/dist/logger-D3krZrNK.d.ts +29 -0
- package/dist/server.d.mts +1515 -0
- package/dist/server.d.ts +1515 -0
- package/dist/server.js +10 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +10 -0
- package/dist/server.mjs.map +1 -0
- package/dist/sse-schema-CK7cUnEo.d.ts +1986 -0
- package/dist/sse-schema-yl5AaSsj.d.mts +1986 -0
- package/dist/types-CVhwLhSN.d.mts +76 -0
- package/dist/types-CVhwLhSN.d.ts +76 -0
- package/package.json +108 -0
- package/src/client/components/business-day-thumb-overlay.tsx +19 -0
- package/src/client/components/daily-report-comment-item.tsx +81 -0
- package/src/client/components/daily-report-comment-section.tsx +166 -0
- package/src/client/components/daily-report-detail-list.tsx +676 -0
- package/src/client/components/daily-report-edit-form.tsx +81 -0
- package/src/client/components/daily-report-list.tsx +1024 -0
- package/src/client/components/daily-report-page.tsx +147 -0
- package/src/client/components/daily-report-resolved-content.tsx +139 -0
- package/src/client/components/unread-indicator.tsx +13 -0
- package/src/client/config-context.tsx +129 -0
- package/src/client/contexts/daily-report-action-context.tsx +910 -0
- package/src/client/hooks/use-daily-report-comments.ts +73 -0
- package/src/client/hooks/use-daily-report-sse-connection.ts +86 -0
- package/src/client/hooks/use-daily-report.spec.ts +155 -0
- package/src/client/hooks/use-daily-report.ts +426 -0
- package/src/client/hooks/use-dynamic-viewport-height.ts +127 -0
- package/src/client/route-helpers.ts +76 -0
- package/src/client/ui/button.tsx +42 -0
- package/src/client/ui/cn.ts +14 -0
- package/src/client/ui/input.tsx +21 -0
- package/src/client/ui/label.tsx +16 -0
- package/src/client/ui/switch.tsx +19 -0
- package/src/client/ui/tabs.tsx +40 -0
- package/src/client/ui/textarea.tsx +19 -0
- package/src/client/utils/constants.ts +29 -0
- package/src/client.ts +21 -0
- package/src/index.ts +10 -0
- package/src/server/cache.ts +165 -0
- package/src/server/etag.ts +18 -0
- package/src/server/external-source.ts +60 -0
- package/src/server/handlers.ts +543 -0
- package/src/server/ports.ts +68 -0
- package/src/server/response.ts +37 -0
- package/src/server/schema.ts +266 -0
- package/src/server/service.spec.ts +97 -0
- package/src/server/service.ts +1308 -0
- package/src/server/sse-reader.spec.ts +55 -0
- package/src/server/sse-reader.ts +223 -0
- package/src/server.ts +83 -0
- package/src/shared/business-date.spec.ts +61 -0
- package/src/shared/business-date.ts +84 -0
- package/src/shared/comment-adapter.ts +78 -0
- package/src/shared/logger.ts +47 -0
- package/src/shared/sse-schema.ts +147 -0
- package/src/shared/text-utils.ts +57 -0
- package/src/shared/types.ts +76 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment management hook merging server data with optimistic local updates.
|
|
3
|
+
* サーバーデータとローカルの楽観的更新を統合して日報コメントを管理するフック。
|
|
4
|
+
*/
|
|
5
|
+
import { useCallback, useMemo, useRef, useState } from "react"
|
|
6
|
+
import { mergeComments, type UIComment } from "../../shared/comment-adapter"
|
|
7
|
+
import type { DailyReportDetail } from "../../shared/types"
|
|
8
|
+
import { useDailyReportActionContext } from "../contexts/daily-report-action-context"
|
|
9
|
+
|
|
10
|
+
type User = { displayName?: string; name?: { familyName?: string; givenName?: string }; emails?: { value: string }[] }
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages daily report comments, integrating server data with optimistic local updates.
|
|
14
|
+
* サーバーデータとローカルの楽観的更新を統合して日報コメントを管理するフック。
|
|
15
|
+
*/
|
|
16
|
+
export const useDailyReportComments = (report: DailyReportDetail | null, user: User | null | undefined, userId?: string | null) => {
|
|
17
|
+
const { pendingAddCommentIds, pendingDeleteCommentIds, resolvedIdMap, version, addComment, deleteComment } = useDailyReportActionContext()
|
|
18
|
+
const [pendingDeleteId, setPendingDeleteId] = useState<number | string | null>(null)
|
|
19
|
+
|
|
20
|
+
const handleDeleteComment = useCallback((cId: number) => report && deleteComment(report.reportHubId, cId, report.date ?? ""), [report, deleteComment])
|
|
21
|
+
const handleAddComment = useCallback((content: string) => (report && user ? addComment(report.reportHubId, content, report.date ?? "") : Promise.resolve()), [report, user, addComment])
|
|
22
|
+
|
|
23
|
+
const prevUiCommentsRef = useRef<UIComment[]>([])
|
|
24
|
+
|
|
25
|
+
// UI用コメントリストの生成: サーバーデータ、レガシーコメント、楽観的更新(追加・削除)をマージして表示用データを構築する。
|
|
26
|
+
// 依存関係: [report, user, userId, pendingDeleteCommentIds, pendingAddCommentIds, resolvedIdMap, version]
|
|
27
|
+
// クリーンアップ: なし
|
|
28
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: version は ref (pending*) 変更時の再計算トリガーとして意図的に依存させる
|
|
29
|
+
const uiComments = useMemo(() => {
|
|
30
|
+
if (!report) return []
|
|
31
|
+
|
|
32
|
+
// 1. Base: Merge legacy and new comments
|
|
33
|
+
// 基本データ: レガシーコメントと新しい形式のコメントをマージ。
|
|
34
|
+
let comments = mergeComments(report.comments, report.commentItems, userId)
|
|
35
|
+
|
|
36
|
+
// 2. Filter: Remove pending deletions
|
|
37
|
+
// フィルタリング: 削除保留中のコメントを除外。
|
|
38
|
+
const deletedIds = pendingDeleteCommentIds.current.get(report.reportHubId)
|
|
39
|
+
if (deletedIds?.size) comments = comments.filter((c) => !deletedIds.has(c.id as number))
|
|
40
|
+
|
|
41
|
+
// 3. Add: Append pending optimistic additions
|
|
42
|
+
// 追加: 楽観的に追加されたコメントを末尾に追加。
|
|
43
|
+
const addedComments = pendingAddCommentIds.current.get(report.reportHubId)
|
|
44
|
+
if (addedComments?.size && user) {
|
|
45
|
+
const existingIds = new Set(comments.map((c) => c.id))
|
|
46
|
+
const authorName = user.displayName?.trim() || (user.name ? `${user.name.familyName ?? ""} ${user.name.givenName ?? ""}`.trim() : "") || user.emails?.[0]?.value || "Unknown"
|
|
47
|
+
|
|
48
|
+
for (const [tempId, content] of addedComments) {
|
|
49
|
+
if (deletedIds?.has(tempId)) continue
|
|
50
|
+
if (existingIds.has(tempId)) continue
|
|
51
|
+
|
|
52
|
+
// Skip if real ID resolved and already present
|
|
53
|
+
// 実IDが解決済みで既にリストに存在する場合はスキップ。
|
|
54
|
+
const realId = resolvedIdMap.current.get(tempId)
|
|
55
|
+
if (realId && existingIds.has(realId)) continue
|
|
56
|
+
|
|
57
|
+
// Skip if content duplicated (server data arrived before ID resolution)
|
|
58
|
+
// 内容が重複している場合(ID解決前にサーバーデータが到達した場合)はスキップ。
|
|
59
|
+
if (comments.some((c) => c.isMine && c.content === content && typeof c.id === "number" && c.id > 0)) continue
|
|
60
|
+
|
|
61
|
+
comments.push({ id: tempId, authorName, content, createdAt: new Date().toISOString(), isMine: true, isLegacy: false })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Stable reference check
|
|
66
|
+
// 参照の安定性チェック: 内容に変更がなければ前回の参照を返すことで不要な再レンダリングを防ぐ。
|
|
67
|
+
if (JSON.stringify(comments) === JSON.stringify(prevUiCommentsRef.current)) return prevUiCommentsRef.current
|
|
68
|
+
prevUiCommentsRef.current = comments
|
|
69
|
+
return comments
|
|
70
|
+
}, [report, user, userId, pendingDeleteCommentIds, pendingAddCommentIds, resolvedIdMap, version])
|
|
71
|
+
|
|
72
|
+
return { uiComments, handleAddComment, handleDeleteComment, pendingDeleteId, setPendingDeleteId, resolvedIdMap: resolvedIdMap.current }
|
|
73
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE connection lifecycle hook for daily-report real-time updates.
|
|
3
|
+
* 日報リアルタイム更新の SSE 接続ライフサイクルフック。
|
|
4
|
+
*/
|
|
5
|
+
import { useEffect } from "react"
|
|
6
|
+
import { createLogger, LogLevel } from "../../shared/logger"
|
|
7
|
+
import { type DailyReportSseMessage, dailyReportSseMessageSchema } from "../../shared/sse-schema"
|
|
8
|
+
import { useDailyReportConfig } from "../config-context"
|
|
9
|
+
|
|
10
|
+
const logger = createLogger(LogLevel.INFO, "daily-report-sse-connection")
|
|
11
|
+
|
|
12
|
+
type UseDailyReportSseConnectionOptions = {
|
|
13
|
+
isSseEnabled: boolean
|
|
14
|
+
onMessage: (payload: Exclude<DailyReportSseMessage, { type: "connected" }>) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Manages Daily Report SSE connection lifecycle with reconnect handling.
|
|
19
|
+
* Handles open, message parse/dispatch, error logging, retry, and cleanup.
|
|
20
|
+
*
|
|
21
|
+
* 日報 SSE 接続のライフサイクルを管理する Hook。
|
|
22
|
+
* 接続開始、メッセージ解析と配送、エラー時再接続、クリーンアップを管理します。
|
|
23
|
+
*/
|
|
24
|
+
export const useDailyReportSseConnection = ({ isSseEnabled, onMessage }: UseDailyReportSseConnectionOptions) => {
|
|
25
|
+
const { ssePath } = useDailyReportConfig()
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Maintains SSE connection while enabled and retries on error.
|
|
29
|
+
* isSseEnabled が true の間は接続を維持し、エラー時は再接続します。
|
|
30
|
+
*/
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!isSseEnabled) return
|
|
33
|
+
|
|
34
|
+
let eventSource: EventSource | null = null
|
|
35
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
36
|
+
let reconnectAttempt = 0
|
|
37
|
+
let lastReceivedEventId: string | undefined
|
|
38
|
+
|
|
39
|
+
const connect = () => {
|
|
40
|
+
eventSource?.close()
|
|
41
|
+
eventSource = new EventSource(`${ssePath}${lastReceivedEventId ? `?lastEventId=${lastReceivedEventId}` : ""}`)
|
|
42
|
+
|
|
43
|
+
eventSource.onopen = () => {
|
|
44
|
+
reconnectAttempt = 0
|
|
45
|
+
logger.info("SSE Connected")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
eventSource.onmessage = (event) => {
|
|
49
|
+
if (event.lastEventId) {
|
|
50
|
+
lastReceivedEventId = event.lastEventId
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const raw = JSON.parse(event.data)
|
|
54
|
+
const result = dailyReportSseMessageSchema.safeParse(raw)
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
logger.error("SSE Validation Error", result.error.format())
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
if (result.data.type === "connected") {
|
|
60
|
+
logger.info("SSE Subscribe Ready")
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
onMessage(result.data)
|
|
64
|
+
} catch (e) {
|
|
65
|
+
logger.error("SSE Parse Error", e)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
eventSource.onerror = (e) => {
|
|
70
|
+
eventSource?.close()
|
|
71
|
+
eventSource = null
|
|
72
|
+
reconnectAttempt++
|
|
73
|
+
const delayMs = Math.min(2000 * 2 ** (reconnectAttempt - 1), 30000)
|
|
74
|
+
logger.error(`SSE Error. Retrying in ${delayMs}ms (attempt ${reconnectAttempt})...`, e)
|
|
75
|
+
reconnectTimer = setTimeout(connect, delayMs)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
connect()
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
if (eventSource) eventSource.close()
|
|
83
|
+
if (reconnectTimer) clearTimeout(reconnectTimer)
|
|
84
|
+
}
|
|
85
|
+
}, [isSseEnabled, onMessage, ssePath])
|
|
86
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
2
|
+
import type { DailyReportDetail } from "../../shared/types"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Unit tests for DailyReportCache public API and getGlobal behavior.
|
|
6
|
+
* DailyReportCache のパブリック API と getGlobal の挙動を検証するユニットテスト。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// テスト用のモック日報データを生成するヘルパー関数
|
|
10
|
+
const makeMockReport = (overrides: Partial<DailyReportDetail> = {}): DailyReportDetail =>
|
|
11
|
+
({
|
|
12
|
+
reportHubId: 1,
|
|
13
|
+
subject: "Test Report",
|
|
14
|
+
content: "Test Content",
|
|
15
|
+
date: "2026-01-01",
|
|
16
|
+
labels: [],
|
|
17
|
+
commentItems: [],
|
|
18
|
+
isRead: false,
|
|
19
|
+
isStar: false,
|
|
20
|
+
updatedAt: new Date().toISOString(),
|
|
21
|
+
...overrides,
|
|
22
|
+
}) as DailyReportDetail
|
|
23
|
+
|
|
24
|
+
describe("DailyReportCache public API", () => {
|
|
25
|
+
// vitest は jsdom 環境で動作するため window オブジェクトが存在する。
|
|
26
|
+
// vi.resetModules() + 動的 import でモジュールを再読み込みし、
|
|
27
|
+
// getGlobal がカスタム TTL を読み込むようにする。
|
|
28
|
+
|
|
29
|
+
let writeCache: typeof import("./use-daily-report").writeCache
|
|
30
|
+
let getCachedReport: typeof import("./use-daily-report").getCachedReport
|
|
31
|
+
let updateDailyReportCache: typeof import("./use-daily-report").updateDailyReportCache
|
|
32
|
+
let applyDailyReportServerUpdates: typeof import("./use-daily-report").applyDailyReportServerUpdates
|
|
33
|
+
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
// モジュールキャッシュをリセットして、getGlobal がカスタム TTL を読み込むようにする
|
|
36
|
+
vi.resetModules()
|
|
37
|
+
// テスト用に短い TTL を設定
|
|
38
|
+
;(window as unknown as Record<string, unknown>).__DAILY_REPORT_CACHE_TTL__ = 5000
|
|
39
|
+
;(window as unknown as Record<string, unknown>).__DAILY_REPORT_CACHE_REVIVE_MS__ = 1000
|
|
40
|
+
|
|
41
|
+
// モジュールを動的にインポート(各テストで独立したキャッシュインスタンスを使用)
|
|
42
|
+
const mod = await import("./use-daily-report")
|
|
43
|
+
writeCache = mod.writeCache
|
|
44
|
+
getCachedReport = mod.getCachedReport
|
|
45
|
+
updateDailyReportCache = mod.updateDailyReportCache
|
|
46
|
+
applyDailyReportServerUpdates = mod.applyDailyReportServerUpdates
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
// window グローバル変数をクリーンアップ
|
|
51
|
+
delete (window as unknown as Record<string, unknown>).__DAILY_REPORT_CACHE_TTL__
|
|
52
|
+
delete (window as unknown as Record<string, unknown>).__DAILY_REPORT_CACHE_REVIVE_MS__
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe("writeCache (DailyReportCache.set)", () => {
|
|
56
|
+
it("レポートを格納し、ID で取得できる", () => {
|
|
57
|
+
const report = makeMockReport()
|
|
58
|
+
writeCache(report, Date.now() + 5000)
|
|
59
|
+
const cached = getCachedReport(1)
|
|
60
|
+
expect(cached).toBeDefined()
|
|
61
|
+
expect(cached?.reportHubId).toBe(1)
|
|
62
|
+
expect(cached?.subject).toBe("Test Report")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("同一 ID の set は完全置換される", () => {
|
|
66
|
+
writeCache(makeMockReport({ subject: "Original" }), Date.now() + 5000)
|
|
67
|
+
writeCache(makeMockReport({ subject: "Replaced", content: "New" }), Date.now() + 5000)
|
|
68
|
+
const cached = getCachedReport(1)
|
|
69
|
+
expect(cached?.subject).toBe("Replaced")
|
|
70
|
+
expect(cached?.content).toBe("New")
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("期限切れエントリは null を返す", () => {
|
|
74
|
+
writeCache(makeMockReport(), Date.now() - 1)
|
|
75
|
+
expect(getCachedReport(1)).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe("updateDailyReportCache (DailyReportCache.update)", () => {
|
|
80
|
+
it("既存レポートの指定フィールドのみを更新し、他は保持する", () => {
|
|
81
|
+
writeCache(makeMockReport({ subject: "Original", content: "Original Content" }), Date.now() + 5000)
|
|
82
|
+
updateDailyReportCache(1, { subject: "Updated Title" } as Partial<DailyReportDetail>)
|
|
83
|
+
const cached = getCachedReport(1)
|
|
84
|
+
expect(cached?.subject).toBe("Updated Title")
|
|
85
|
+
expect(cached?.content).toBe("Original Content")
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("updatedAt が古い更新はスキップされる (stale guard)", () => {
|
|
89
|
+
writeCache(makeMockReport({ subject: "Current", updatedAt: "2026-01-02T00:00:00Z" }), Date.now() + 5000)
|
|
90
|
+
updateDailyReportCache(1, {
|
|
91
|
+
subject: "Stale",
|
|
92
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
93
|
+
} as Partial<DailyReportDetail>)
|
|
94
|
+
expect(getCachedReport(1)?.subject).toBe("Current")
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("未キャッシュの ID を更新しても何も起きない", () => {
|
|
98
|
+
updateDailyReportCache(999, { subject: "Ghost" } as Partial<DailyReportDetail>)
|
|
99
|
+
expect(getCachedReport(999)).toBeNull()
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe("applyDailyReportServerUpdates", () => {
|
|
104
|
+
it("キャッシュ済みレポートを完全置換する", () => {
|
|
105
|
+
writeCache(makeMockReport({ subject: "Old", content: "Old" }), Date.now() + 1000)
|
|
106
|
+
applyDailyReportServerUpdates(1, makeMockReport({ subject: "Fresh", content: "Fresh" }))
|
|
107
|
+
const cached = getCachedReport(1)
|
|
108
|
+
expect(cached?.subject).toBe("Fresh")
|
|
109
|
+
expect(cached?.content).toBe("Fresh")
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("未キャッシュの ID でも新規エントリとして格納される", () => {
|
|
113
|
+
applyDailyReportServerUpdates(42, makeMockReport({ reportHubId: 42, subject: "New" }))
|
|
114
|
+
expect(getCachedReport(42)?.subject).toBe("New")
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe("getGlobal behavior (window グローバル変数の読み取り)", () => {
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
delete (window as unknown as Record<string, unknown>).__DAILY_REPORT_CACHE_TTL__
|
|
122
|
+
delete (window as unknown as Record<string, unknown>).__DAILY_REPORT_CACHE_REVIVE_MS__
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it("window に正の数値が設定されていれば、その値が使われる", async () => {
|
|
126
|
+
vi.resetModules()
|
|
127
|
+
;(window as unknown as Record<string, unknown>).__DAILY_REPORT_CACHE_TTL__ = 7777
|
|
128
|
+
|
|
129
|
+
const mod = await import("./use-daily-report")
|
|
130
|
+
const report = makeMockReport()
|
|
131
|
+
mod.writeCache(report, Date.now() + 7777)
|
|
132
|
+
expect(mod.getCachedReport(1)).toBeDefined()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it("window が undefined ならデフォルト値 (300000ms) が使われる", async () => {
|
|
136
|
+
vi.resetModules()
|
|
137
|
+
delete (window as unknown as Record<string, unknown>).__DAILY_REPORT_CACHE_TTL__
|
|
138
|
+
|
|
139
|
+
const mod = await import("./use-daily-report")
|
|
140
|
+
const report = makeMockReport()
|
|
141
|
+
mod.writeCache(report, Date.now() + 300_000)
|
|
142
|
+
expect(mod.getCachedReport(1)).toBeDefined()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("window に 0 が設定されていれば 0 が使われる (|| バグの修正確認)", async () => {
|
|
146
|
+
vi.resetModules()
|
|
147
|
+
;(window as unknown as Record<string, unknown>).__DAILY_REPORT_CACHE_TTL__ = 0
|
|
148
|
+
|
|
149
|
+
const mod = await import("./use-daily-report")
|
|
150
|
+
const report = makeMockReport()
|
|
151
|
+
// TTL=0ms → expiresAt = Date.now() + 0 → 即時期限切れ → getCachedReport は null
|
|
152
|
+
mod.writeCache(report, Date.now() + 0)
|
|
153
|
+
expect(mod.getCachedReport(1)).toBeNull()
|
|
154
|
+
})
|
|
155
|
+
})
|