@aiquants/daily-report 0.7.0 → 0.9.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 +13 -6
- package/dist/client.d.mts +438 -26
- package/dist/client.d.ts +438 -26
- package/dist/client.js +5 -4
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +5 -4
- package/dist/client.mjs.map +1 -1
- package/dist/index.d.mts +115 -3
- package/dist/index.d.ts +115 -3
- 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 +15 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.js +9 -6
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +9 -6
- package/dist/server.mjs.map +1 -1
- package/dist/{sse-schema-Df49KA7B.d.mts → sse-schema-BiUnufrI.d.mts} +12 -0
- package/dist/{sse-schema-RHckD7TS.d.ts → sse-schema-YZh7Pz7i.d.ts} +12 -0
- package/dist/styles/daily-report.standalone.css +1 -1
- package/package.json +3 -3
- package/src/client/components/daily-report-comment-item.tsx +6 -1
- package/src/client/components/daily-report-detail-list.tsx +65 -34
- package/src/client/components/daily-report-edit-form.tsx +6 -0
- package/src/client/components/daily-report-ids-stream-status.spec.tsx +93 -0
- package/src/client/components/daily-report-ids-stream-status.tsx +67 -0
- package/src/client/components/daily-report-list.tsx +88 -55
- package/src/client/components/daily-report-page.spec.tsx +119 -0
- package/src/client/components/daily-report-page.tsx +62 -69
- package/src/client/components/daily-report-resolved-content.tsx +36 -10
- package/src/client/components/report-views.spec.tsx +37 -2
- package/src/client/config-context.tsx +18 -0
- package/src/client/contexts/daily-report-action-context.spec.tsx +278 -0
- package/src/client/contexts/daily-report-action-context.tsx +203 -65
- package/src/client/contexts/daily-report-error-context.spec.tsx +66 -0
- package/src/client/contexts/daily-report-error-context.tsx +98 -0
- package/src/client/hooks/use-animated-number.spec.ts +103 -0
- package/src/client/hooks/use-animated-number.ts +69 -0
- package/src/client/hooks/use-daily-report.spec.ts +50 -2
- package/src/client/hooks/use-daily-report.ts +30 -1
- package/src/client/route-helpers.spec.ts +100 -0
- package/src/client/route-helpers.ts +20 -41
- package/src/client/streaming/daily-report-ids-stream-client.spec.ts +704 -0
- package/src/client/streaming/daily-report-ids-stream-client.ts +677 -0
- package/src/client/streaming/daily-report-ids-stream-session.spec.ts +360 -0
- package/src/client/streaming/daily-report-ids-stream-session.ts +280 -0
- package/src/client.ts +5 -0
- package/src/index.ts +1 -0
- package/src/server/handlers.action.spec.ts +44 -0
- package/src/server/handlers.ids-stream.spec.ts +414 -0
- package/src/server/handlers.ts +324 -8
- package/src/server/service.spec.ts +83 -7
- package/src/server/service.ts +100 -3
- package/src/server/test-helpers/handlers-config.ts +48 -1
- package/src/server.spec.ts +49 -0
- package/src/server.ts +7 -0
- package/src/shared/business-date.spec.ts +22 -1
- package/src/shared/business-date.ts +20 -0
- package/src/shared/comment-adapter.spec.ts +38 -0
- package/src/shared/comment-adapter.ts +9 -7
- package/src/shared/ids-stream.spec.ts +65 -0
- package/src/shared/ids-stream.ts +139 -0
- package/src/shared/sse-schema.ts +5 -0
- package/src/shared/text-utils.spec.ts +42 -0
- package/src/shared/text-utils.ts +8 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire contract and ordering helpers for the daily-report ids NDJSON stream.
|
|
3
|
+
* 日報 ID 一覧 NDJSON ストリームのワイヤ契約と並び順ヘルパー。
|
|
4
|
+
*
|
|
5
|
+
* サーバーは `GET {apiBasePath}/ids-stream` で 1 行 1 JSON の NDJSON を返す。
|
|
6
|
+
* 行は原則 {@link DailyReportIdsStreamChunkLine} で、送出中に予期しない失敗が
|
|
7
|
+
* 起きた場合のみ {@link DailyReportIdsStreamErrorLine} を 1 行流して正常クローズする
|
|
8
|
+
* (クライアントは「isLast 無しで終端」を再開シグナルとして扱う)。
|
|
9
|
+
*
|
|
10
|
+
* 並び順は SQL の `ORDER BY business_date DESC, id DESC` (MSSQL の DESC は NULL を
|
|
11
|
+
* 末尾に置く) と完全一致させる。カーソル再開はこの全順序に対する「厳密に後」の
|
|
12
|
+
* 位置からのスライスとして定義する。
|
|
13
|
+
*/
|
|
14
|
+
import type { DailyReportItem } from "./types"
|
|
15
|
+
|
|
16
|
+
/** ストリーム再開カーソル。直前チャンク末尾アイテムの並び順キー。 */
|
|
17
|
+
export type DailyReportIdsStreamCursor = {
|
|
18
|
+
/** 営業日 (YYYY-MM-DD)。null は「営業日なし領域」(並び順の末尾) を表す。 */
|
|
19
|
+
businessDate: string | null
|
|
20
|
+
/** 日報 Hub ID。 */
|
|
21
|
+
reportHubId: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** データを運ぶ NDJSON 行。 */
|
|
25
|
+
export type DailyReportIdsStreamChunkLine = {
|
|
26
|
+
/** このチャンクのアイテム群 (並び順どおり)。 */
|
|
27
|
+
items: DailyReportItem[]
|
|
28
|
+
/** 次チャンクの再開カーソル。最終チャンクでは null。 */
|
|
29
|
+
nextCursor: DailyReportIdsStreamCursor | null
|
|
30
|
+
/** これが最終チャンクかどうか。 */
|
|
31
|
+
isLast: boolean
|
|
32
|
+
/** 全体件数。接続ごとの先頭行にのみ載る。 */
|
|
33
|
+
total?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** サーバー側の予期しない失敗を伝える NDJSON 行 (内部情報は含めない)。 */
|
|
37
|
+
export type DailyReportIdsStreamErrorLine = {
|
|
38
|
+
error: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** NDJSON 1 行のユニオン。 */
|
|
42
|
+
export type DailyReportIdsStreamLine = DailyReportIdsStreamChunkLine | DailyReportIdsStreamErrorLine
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Type guard for chunk lines (anything else is treated as an error line).
|
|
46
|
+
* チャンク行の型ガード (それ以外はエラー行として扱う)。
|
|
47
|
+
*
|
|
48
|
+
* @param line Parsed NDJSON line. パース済みの NDJSON 行。
|
|
49
|
+
* @returns True when the line carries items. アイテムを運ぶ行なら true。
|
|
50
|
+
*/
|
|
51
|
+
export const isIdsStreamChunkLine = (line: unknown): line is DailyReportIdsStreamChunkLine => {
|
|
52
|
+
return typeof line === "object" && line !== null && Array.isArray((line as { items?: unknown }).items)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Total order of the ids stream: businessDate DESC (NULL last), then id DESC.
|
|
57
|
+
* ids ストリームの全順序。営業日降順 (NULL は末尾)、同日内は ID 降順。
|
|
58
|
+
*
|
|
59
|
+
* MSSQL の `ORDER BY business_date DESC, id DESC` と一致させることが唯一の存在理由。
|
|
60
|
+
* 営業日は正規化済みの `YYYY-MM-DD` 文字列なので辞書順比較 = 時系列比較になる。
|
|
61
|
+
*
|
|
62
|
+
* @param a Left key. 左辺キー。
|
|
63
|
+
* @param b Right key. 右辺キー。
|
|
64
|
+
* @returns Negative when `a` streams before `b`. `a` が先に流れるなら負値。
|
|
65
|
+
*/
|
|
66
|
+
export const compareIdsStreamOrder = (a: DailyReportIdsStreamCursor, b: DailyReportIdsStreamCursor): number => {
|
|
67
|
+
if (a.businessDate !== b.businessDate) {
|
|
68
|
+
// null 営業日は並び順の末尾 (MSSQL DESC の NULL 末尾挙動へ揃える)
|
|
69
|
+
if (a.businessDate === null) return 1
|
|
70
|
+
if (b.businessDate === null) return -1
|
|
71
|
+
return a.businessDate > b.businessDate ? -1 : 1
|
|
72
|
+
}
|
|
73
|
+
return b.reportHubId - a.reportHubId
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Checks whether items already follow the ids-stream total order.
|
|
78
|
+
* アイテム配列が ids ストリームの全順序に既に従っているかを検査する処理。
|
|
79
|
+
*
|
|
80
|
+
* ストリームはソート順どおりに届いて末尾へ追記されるため通常は整列済みで、
|
|
81
|
+
* この O(n) 検査で全件ソート (O(n log n)) を省略できる。
|
|
82
|
+
*
|
|
83
|
+
* @param items Items to probe. 検査対象のアイテム配列。
|
|
84
|
+
* @returns True when already ordered. 整列済みなら true。
|
|
85
|
+
*/
|
|
86
|
+
export const isIdsStreamOrdered = (items: readonly DailyReportItem[]): boolean => {
|
|
87
|
+
for (let i = 1; i < items.length; i++) {
|
|
88
|
+
if (compareIdsStreamOrder(items[i - 1], items[i]) > 0) return false
|
|
89
|
+
}
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Returns a copy with the item upserted at its ids-stream-ordered position.
|
|
95
|
+
* アイテムを ids ストリーム順の正しい位置へ upsert した複製配列を返す処理。
|
|
96
|
+
*
|
|
97
|
+
* 前提: `items` は ids ストリーム順に整列済み (このモジュールの比較器を使う全書き込み元が
|
|
98
|
+
* 維持する不変条件)。既存の同 ID は除去してから二分探索で挿入位置を求めるため、
|
|
99
|
+
* 全件ソート無しの O(n) で並び順を保てる (以前は SSE 1 件ごとに 20 万件超を
|
|
100
|
+
* localeCompare で全ソートしていた)。
|
|
101
|
+
*
|
|
102
|
+
* @param items Ordered items. 整列済みアイテム配列。
|
|
103
|
+
* @param item Item to upsert. upsert するアイテム。
|
|
104
|
+
* @returns New ordered array. 新しい整列済み配列。
|
|
105
|
+
*/
|
|
106
|
+
export const upsertInIdsStreamOrder = (items: readonly DailyReportItem[], item: DailyReportItem): DailyReportItem[] => {
|
|
107
|
+
// 既存の同 ID を除いた複製 (位置移動を伴う更新もこれで単一経路になる)
|
|
108
|
+
const next = items.filter((existing) => existing.reportHubId !== item.reportHubId)
|
|
109
|
+
let low = 0
|
|
110
|
+
let high = next.length
|
|
111
|
+
while (low < high) {
|
|
112
|
+
const mid = (low + high) >> 1
|
|
113
|
+
if (compareIdsStreamOrder(next[mid], item) <= 0) {
|
|
114
|
+
low = mid + 1
|
|
115
|
+
} else {
|
|
116
|
+
high = mid
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
next.splice(low, 0, item)
|
|
120
|
+
return next
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Finds the first index that streams strictly after the cursor.
|
|
125
|
+
* カーソルより厳密に後ろへ並ぶ最初のインデックスを求める処理。
|
|
126
|
+
*
|
|
127
|
+
* 配列は必ずストリーム順 (SQL の ORDER BY) で渡されるが、比較器とのズレが
|
|
128
|
+
* あっても壊れない線形走査で決定する (件数は高々数万・再開時のみの実行)。
|
|
129
|
+
* 該当なし (カーソルが末尾以降) は `items.length` を返し、空チャンク +
|
|
130
|
+
* `isLast: true` の正常終端に落ちる。
|
|
131
|
+
*
|
|
132
|
+
* @param items Stream-ordered items. ストリーム順のアイテム配列。
|
|
133
|
+
* @param cursor Resume cursor. 再開カーソル。
|
|
134
|
+
* @returns Index to resume emission from. 送出を再開するインデックス。
|
|
135
|
+
*/
|
|
136
|
+
export const findIdsStreamResumeIndex = (items: readonly DailyReportItem[], cursor: DailyReportIdsStreamCursor): number => {
|
|
137
|
+
const index = items.findIndex((item) => compareIdsStreamOrder({ businessDate: item.businessDate, reportHubId: item.reportHubId }, cursor) > 0)
|
|
138
|
+
return index === -1 ? items.length : index
|
|
139
|
+
}
|
package/src/shared/sse-schema.ts
CHANGED
|
@@ -108,6 +108,9 @@ export const commentAddMessageSchema = z.object({
|
|
|
108
108
|
reportHubId: z.number(),
|
|
109
109
|
comment: dailyReportCommentItemSchema,
|
|
110
110
|
clientTempId: z.string(),
|
|
111
|
+
// 下書き日報のコメントは所有者以外に見えてはならない (本文の全員配信を防ぐ)。
|
|
112
|
+
// サーバーサイドフィルタ専用で、SSE ルートがフィルタ後に除去する
|
|
113
|
+
recipientRawUserId: z.number().optional(),
|
|
111
114
|
})
|
|
112
115
|
|
|
113
116
|
export const commentDeleteMessageSchema = z.object({
|
|
@@ -115,6 +118,8 @@ export const commentDeleteMessageSchema = z.object({
|
|
|
115
118
|
reportHubId: z.number(),
|
|
116
119
|
commentId: z.number(),
|
|
117
120
|
clientTempId: z.string(),
|
|
121
|
+
// comment-add と同じ理由の宛先フィルタ
|
|
122
|
+
recipientRawUserId: z.number().optional(),
|
|
118
123
|
})
|
|
119
124
|
|
|
120
125
|
export const reportCreateMessageSchema = z.object({
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for shared text helpers.
|
|
3
|
+
* 共通テキストヘルパーのテスト。
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from "vitest"
|
|
6
|
+
import { fallbackText, formatToIsoDate } from "./text-utils"
|
|
7
|
+
|
|
8
|
+
describe("text-utils", () => {
|
|
9
|
+
describe("fallbackText", () => {
|
|
10
|
+
it("returns a dash for empty / nullish values", () => {
|
|
11
|
+
expect(fallbackText(null)).toBe("-")
|
|
12
|
+
expect(fallbackText(undefined)).toBe("-")
|
|
13
|
+
expect(fallbackText(" ")).toBe("-")
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("trims and returns real text", () => {
|
|
17
|
+
expect(fallbackText(" hello ")).toBe("hello")
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe("formatToIsoDate", () => {
|
|
22
|
+
it("returns a date-only string verbatim WITHOUT a UTC round-trip", () => {
|
|
23
|
+
// `new Date('2026-08-08')` は UTC 深夜と解釈され、負オフセット圏では前日へずれる。
|
|
24
|
+
// 日付のみは Date を経由せず、どのタイムゾーンでも同じ値を返さなければならない。
|
|
25
|
+
expect(formatToIsoDate("2026-08-08")).toBe("2026-08-08")
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("normalizes slash date-only to hyphenated", () => {
|
|
29
|
+
expect(formatToIsoDate("2026/08/08")).toBe("2026-08-08")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("keeps the time portion for datetime strings", () => {
|
|
33
|
+
expect(formatToIsoDate("2026-08-08T13:05:09")).toBe("2026-08-08T13:05:09")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("returns a dash for empty and the raw string for unparseable input", () => {
|
|
37
|
+
expect(formatToIsoDate("")).toBe("-")
|
|
38
|
+
expect(formatToIsoDate(null)).toBe("-")
|
|
39
|
+
expect(formatToIsoDate("not-a-date")).toBe("not-a-date")
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
})
|
package/src/shared/text-utils.ts
CHANGED
|
@@ -35,6 +35,14 @@ export const formatToIsoDate = (value: unknown): string => {
|
|
|
35
35
|
return "-"
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// 日付のみ (YYYY-MM-DD / YYYY/MM/DD) は Date を経由せず整形する。
|
|
39
|
+
// `new Date("2026-08-08")` は UTC 深夜と解釈され、ローカル getter で負オフセット圏
|
|
40
|
+
// (米国など) では前日へずれるため。
|
|
41
|
+
const dateOnly = trimmed.match(/^(\d{4})[-/](\d{2})[-/](\d{2})$/)
|
|
42
|
+
if (dateOnly) {
|
|
43
|
+
return `${dateOnly[1]}-${dateOnly[2]}-${dateOnly[3]}`
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
const date = new Date(trimmed)
|
|
39
47
|
if (Number.isNaN(date.getTime())) {
|
|
40
48
|
return trimmed
|