profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
sessionStore.ts 3,943 bytes
1 import { randomUUID } from 'node:crypto'
2 import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
3 import path from 'node:path'
4 import {
5 SessionSchema,
6 initialCoverage,
7 type Coverage,
8 type Segment,
9 type Session,
10 type SessionStatus,
11 type SessionSummary,
12 type Speaker,
13 } from '../../shared/types'
14
15 export class SessionStore {
16 constructor(private readonly baseDir: string) {}
17
18 private sessionDir(id: string): string {
19 return path.join(this.baseDir, id)
20 }
21
22 private sessionFile(id: string): string {
23 return path.join(this.sessionDir(id), 'session.json')
24 }
25
26 private async writeSession(session: Session): Promise<void> {
27 const dir = this.sessionDir(session.id)
28 await mkdir(dir, { recursive: true })
29 const file = this.sessionFile(session.id)
30 const tmp = path.join(dir, `.session-${randomUUID()}.tmp`)
31 await writeFile(tmp, JSON.stringify(session, null, 2), 'utf8')
32 await rename(tmp, file)
33 }
34
35 async createSession(name: string, targetDir: string): Promise<Session> {
36 const session: Session = {
37 id: randomUUID(),
38 name,
39 targetDir,
40 createdAt: new Date().toISOString(),
41 segments: [],
42 coverage: initialCoverage(),
43 summary: '',
44 status: 'interviewing',
45 openBlockers: [],
46 }
47 await this.writeSession(session)
48 return session
49 }
50
51 async getSession(id: string): Promise<Session | null> {
52 try {
53 const raw = await readFile(this.sessionFile(id), 'utf8')
54 return SessionSchema.parse(JSON.parse(raw))
55 } catch (err) {
56 if (isNotFound(err)) return null
57 throw err
58 }
59 }
60
61 async listSessions(): Promise<SessionSummary[]> {
62 let entries: string[]
63 try {
64 entries = await readdir(this.baseDir)
65 } catch (err) {
66 if (isNotFound(err)) return []
67 throw err
68 }
69 const summaries: SessionSummary[] = []
70 for (const id of entries) {
71 const session = await this.getSession(id)
72 if (session) {
73 summaries.push({
74 id: session.id,
75 name: session.name,
76 status: session.status,
77 createdAt: session.createdAt,
78 })
79 }
80 }
81 summaries.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
82 return summaries
83 }
84
85 async deleteSession(id: string): Promise<boolean> {
86 const session = await this.getSession(id)
87 if (!session) return false
88 await rm(this.sessionDir(id), { recursive: true, force: true })
89 return true
90 }
91
92 async appendSegment(
93 id: string,
94 speaker: Speaker,
95 text: string,
96 ): Promise<{ session: Session; segment: Segment }> {
97 const session = await this.getSession(id)
98 if (!session) throw new Error(`session not found: ${id}`)
99 const segment: Segment = {
100 id: `S${session.segments.length + 1}`,
101 ts: new Date().toISOString(),
102 speaker,
103 text,
104 }
105 session.segments.push(segment)
106 await this.writeSession(session)
107 return { session, segment }
108 }
109
110 async updateTurn(
111 id: string,
112 update: {
113 coverage?: Coverage
114 summary?: string
115 status?: SessionStatus
116 openBlockers?: string[]
117 },
118 ): Promise<Session> {
119 const session = await this.getSession(id)
120 if (!session) throw new Error(`session not found: ${id}`)
121 if (update.coverage) session.coverage = update.coverage
122 if (update.summary !== undefined) session.summary = update.summary
123 if (update.status) session.status = update.status
124 if (update.openBlockers) session.openBlockers = update.openBlockers
125 await this.writeSession(session)
126 return session
127 }
128 }
129
130 function isNotFound(err: unknown): boolean {
131 return typeof err === 'object' && err !== null && 'code' in err && (err as { code?: string }).code === 'ENOENT'
132 }
133
134 export function createDefaultSessionStore(): SessionStore {
135 const baseDir = process.env.DATA_DIR ?? path.join(process.cwd(), 'data', 'sessions')
136 return new SessionStore(baseDir)
137 }
138