Skip to content

Commit 277e58d

Browse files
committed
Adds type-safe JSON serialization utility
Updates all schedule-related API handlers to use async/await pattern consistently, ensuring data is fully resolved before sending responses.
1 parent 80bf95f commit 277e58d

File tree

2 files changed

+57
-22
lines changed

2 files changed

+57
-22
lines changed

trmnl-ha/ha-trmnl/lib/http-router.ts

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import { loadDevicesConfig, loadPresets } from '../devices.js'
2525
import type { BrowserFacade } from './browserFacade.js'
2626
import type { ScheduleInput, ScheduleUpdate } from '../types/domain.js'
27+
import { toJson } from './json.js'
2728

2829
const __filename = fileURLToPath(import.meta.url)
2930
const __dirname = dirname(__filename)
@@ -139,7 +140,7 @@ export class HttpRouter {
139140

140141
response.writeHead(httpStatus, { 'Content-Type': 'application/json' })
141142
response.end(
142-
JSON.stringify({
143+
toJson({
143144
status,
144145
uptime: process.uptime(),
145146
timestamp: new Date().toISOString(),
@@ -157,28 +158,28 @@ export class HttpRouter {
157158
response.setHeader('Content-Type', 'application/json')
158159

159160
if (request.method === 'GET') {
160-
const schedules = loadSchedules()
161+
const schedules = await loadSchedules()
161162
response.writeHead(200)
162-
response.end(JSON.stringify(schedules))
163+
response.end(toJson(schedules))
163164
return true
164165
}
165166

166167
if (request.method === 'POST') {
167168
try {
168169
const body = await this.#readRequestBody(request)
169170
const schedule = JSON.parse(body) as ScheduleInput
170-
const created = createSchedule(schedule)
171+
const created = await createSchedule(schedule)
171172
response.writeHead(201)
172-
response.end(JSON.stringify(created))
173+
response.end(toJson(created))
173174
} catch (err) {
174175
response.writeHead(400)
175-
response.end(JSON.stringify({ error: (err as Error).message }))
176+
response.end(toJson({ error: (err as Error).message }))
176177
}
177178
return true
178179
}
179180

180181
response.writeHead(405)
181-
response.end(JSON.stringify({ error: 'Method not allowed' }))
182+
response.end(toJson({ error: 'Method not allowed' }))
182183
return true
183184
}
184185

@@ -195,39 +196,39 @@ export class HttpRouter {
195196
try {
196197
const body = await this.#readRequestBody(request)
197198
const updates = JSON.parse(body) as ScheduleUpdate
198-
const updated = updateSchedule(id, updates)
199+
const updated = await updateSchedule(id, updates)
199200

200201
if (!updated) {
201202
response.writeHead(404)
202-
response.end(JSON.stringify({ error: 'Schedule not found' }))
203+
response.end(toJson({ error: 'Schedule not found' }))
203204
return true
204205
}
205206

206207
response.writeHead(200)
207-
response.end(JSON.stringify(updated))
208+
response.end(toJson(updated))
208209
} catch (err) {
209210
response.writeHead(400)
210-
response.end(JSON.stringify({ error: (err as Error).message }))
211+
response.end(toJson({ error: (err as Error).message }))
211212
}
212213
return true
213214
}
214215

215216
if (request.method === 'DELETE') {
216-
const deleted = deleteSchedule(id)
217+
const deleted = await deleteSchedule(id)
217218

218219
if (!deleted) {
219220
response.writeHead(404)
220-
response.end(JSON.stringify({ error: 'Schedule not found' }))
221+
response.end(toJson({ error: 'Schedule not found' }))
221222
return true
222223
}
223224

224225
response.writeHead(200)
225-
response.end(JSON.stringify({ success: true }))
226+
response.end(toJson({ success: true }))
226227
return true
227228
}
228229

229230
response.writeHead(405)
230-
response.end(JSON.stringify({ error: 'Method not allowed' }))
231+
response.end(toJson({ error: 'Method not allowed' }))
231232
return true
232233
}
233234

@@ -240,13 +241,13 @@ export class HttpRouter {
240241

241242
if (request.method !== 'POST') {
242243
response.writeHead(405)
243-
response.end(JSON.stringify({ error: 'Method not allowed' }))
244+
response.end(toJson({ error: 'Method not allowed' }))
244245
return true
245246
}
246247

247248
if (!this.#scheduler) {
248249
response.writeHead(503)
249-
response.end(JSON.stringify({ error: 'Scheduler not available' }))
250+
response.end(toJson({ error: 'Scheduler not available' }))
250251
return true
251252
}
252253

@@ -256,13 +257,13 @@ export class HttpRouter {
256257
try {
257258
const result = await this.#scheduler.executeNow(id)
258259
response.writeHead(200)
259-
response.end(JSON.stringify(result))
260+
response.end(toJson(result))
260261
} catch (err) {
261262
console.error('Error executing schedule manually:', err)
262263
response.writeHead(
263264
(err as Error).message.includes('not found') ? 404 : 500
264265
)
265-
response.end(JSON.stringify({ error: (err as Error).message }))
266+
response.end(toJson({ error: (err as Error).message }))
266267
}
267268

268269
return true
@@ -271,14 +272,14 @@ export class HttpRouter {
271272
#handleDevicesAPI(response: ServerResponse): boolean {
272273
const devices = loadDevicesConfig()
273274
response.writeHead(200, { 'Content-Type': 'application/json' })
274-
response.end(JSON.stringify(devices))
275+
response.end(toJson(devices))
275276
return true
276277
}
277278

278279
#handlePresetsAPI(response: ServerResponse): boolean {
279280
const presets = loadPresets()
280281
response.writeHead(200, { 'Content-Type': 'application/json' })
281-
response.end(JSON.stringify(presets))
282+
response.end(toJson(presets))
282283
return true
283284
}
284285

@@ -313,7 +314,9 @@ export class HttpRouter {
313314
}
314315

315316
const contentLength =
316-
typeof content === 'string' ? Buffer.byteLength(content) : content.length
317+
typeof content === 'string'
318+
? Buffer.byteLength(content)
319+
: content.length
317320

318321
response.writeHead(200, {
319322
'Content-Type': contentType,

trmnl-ha/ha-trmnl/lib/json.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Type-safe JSON serialization utilities.
3+
*
4+
* Prevents accidentally serializing Promises or other non-data types.
5+
*
6+
* @module lib/json
7+
*/
8+
9+
/**
10+
* Serializable data - excludes Promises and functions at compile time.
11+
*
12+
* Uses a conditional type: if T extends Promise, it resolves to `never`,
13+
* causing a type error. Otherwise accepts the value as-is.
14+
*/
15+
type Serializable<T> = T extends Promise<unknown> ? never : T
16+
17+
/**
18+
* Type-safe JSON.stringify that rejects Promises at compile time.
19+
*
20+
* Standard JSON.stringify accepts `any`, so passing an un-awaited Promise
21+
* compiles fine but serializes to `{}`. This wrapper catches that mistake.
22+
*
23+
* @example
24+
* // Compile error - Promise resolves to 'never'
25+
* toJson(loadSchedules())
26+
*
27+
* // Works - awaited value is Schedule[]
28+
* toJson(await loadSchedules())
29+
*/
30+
export function toJson<T>(data: Serializable<T>): string {
31+
return JSON.stringify(data)
32+
}

0 commit comments

Comments
 (0)