Refactor internal APIs

This commit is contained in:
Lars Strojny 2022-11-14 01:20:53 +01:00
parent 4fad222b82
commit 479fbe5727
11 changed files with 51 additions and 75 deletions

View file

@ -1,14 +1,9 @@
import type { Device } from '../../boundaries/hap' import type { Config, Device } from '../../boundaries'
import type { Logger } from 'homebridge' import type { Logger } from 'homebridge'
type Pin = string export interface HapDiscoveryConfig {
config: Pick<Config, 'debug' | 'pin' | 'refresh_interval' | 'discovery_timeout' | 'request_timeout'>
export interface HapConfig { log: Logger
pin: Pin
refreshInterval: number
discoveryTimeout: number
requestTimeout: number
logger: Logger
debug: boolean
} }
export type HapDiscover = (config: HapConfig) => Promise<Device[]>
export type HapDiscover = (config: HapDiscoveryConfig) => Promise<Device[]>

View file

@ -1,25 +1,18 @@
import type { HapDiscover } from './api' import type { HapDiscover } from './api'
import { HAPNodeJSClient } from 'hap-node-client' import { HAPNodeJSClient, type HAPNodeJSClientConfig } from 'hap-node-client'
import { type Device, DeviceBoundary, checkBoundary } from '../../boundaries' import { type Device, DeviceBoundary, checkBoundary } from '../../boundaries'
import type { Logger } from 'homebridge' import type { Logger } from 'homebridge'
import z from 'zod' import z from 'zod'
const MaybeDevices = z.array(z.unknown()) const MaybeDevices = z.array(z.unknown())
interface HapConfig {
debug: boolean
refresh: number
timeout: number
reqTimeout: number
pin: string
}
type ResolveFunc = (devices: Device[]) => void type ResolveFunc = (devices: Device[]) => void
type RejectFunc = (error: unknown) => void type RejectFunc = (error: unknown) => void
const clientMap: Record<string, HAPNodeJSClient> = {} const clientMap: Record<string, HAPNodeJSClient> = {}
const promiseMap: Record<string, [ResolveFunc, RejectFunc]> = {} const promiseMap: Record<string, [ResolveFunc, RejectFunc]> = {}
function startDiscovery(logger: Logger, config: HapConfig, resolve: ResolveFunc, reject: RejectFunc) { function startDiscovery(logger: Logger, config: HAPNodeJSClientConfig, resolve: ResolveFunc, reject: RejectFunc) {
const key = JSON.stringify(config) const key = JSON.stringify(config)
if (!clientMap[key]) { if (!clientMap[key]) {
@ -49,16 +42,16 @@ function startDiscovery(logger: Logger, config: HapConfig, resolve: ResolveFunc,
promiseMap[key] = [resolve, reject] promiseMap[key] = [resolve, reject]
} }
export const discover: HapDiscover = ({ pin, refreshInterval, discoveryTimeout, requestTimeout, logger, debug }) => { export const hapNodeJsClientDiscover: HapDiscover = ({ config, log }) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
startDiscovery( startDiscovery(
logger, log,
{ {
debug: debug, debug: config.debug,
refresh: refreshInterval, refresh: config.refresh_interval,
timeout: discoveryTimeout, timeout: config.discovery_timeout,
reqTimeout: requestTimeout, reqTimeout: config.request_timeout,
pin, pin: config.pin,
}, },
resolve, resolve,
reject, reject,

View file

@ -0,0 +1,2 @@
export * from './api'
export * from './hap_node_js_client'

View file

@ -1,4 +1,7 @@
import type { HttpServer } from '../../http' import type { Logger } from 'homebridge'
import type { RequestListener, Server } from 'http'
import type { Config } from '../../boundaries'
import type { Metric } from '../../metrics'
export interface HttpResponse { export interface HttpResponse {
statusCode?: number statusCode?: number
@ -11,3 +14,16 @@ export interface HttpServerController {
} }
export type HttpAdapter = (config: HttpServer) => Promise<HttpServerController> export type HttpAdapter = (config: HttpServer) => Promise<HttpServerController>
export type HttpConfig = Pick<Config, 'debug' | 'port' | 'prefix'>
export interface HttpServer {
log?: Logger
config: HttpConfig
serverFactory?: (requestListener: RequestListener) => Server
onRequest(): HttpResponse | undefined
onMetrics(): HttpResponse
onNotFound(): HttpResponse
onError(error: unknown): HttpResponse
updateMetrics(metrics: Metric[]): void
}

View file

@ -1,6 +1,5 @@
import Fastify, { type FastifyReply, type FastifyRequest, type HookHandlerDoneFunction } from 'fastify' import Fastify, { type FastifyReply, type FastifyRequest, type HookHandlerDoneFunction } from 'fastify'
import type { HttpAdapter, HttpResponse } from './api' import type { HttpAdapter, HttpResponse, HttpServer } from './api'
import type { HttpServer } from '../../http'
function adaptResponseToReply(response: HttpResponse, reply: FastifyReply): void { function adaptResponseToReply(response: HttpResponse, reply: FastifyReply): void {
if (response.statusCode) { if (response.statusCode) {
@ -23,7 +22,7 @@ function formatCombinedLog(request: FastifyRequest, reply: FastifyReply): string
return `${remoteAddress} - "${request.method} ${request.url} HTTP/${request.raw.httpVersion}" ${reply.statusCode} "${request.protocol}://${request.hostname}" "${userAgent}" "${contentType}"` return `${remoteAddress} - "${request.method} ${request.url} HTTP/${request.raw.httpVersion}" ${reply.statusCode} "${request.protocol}://${request.hostname}" "${userAgent}" "${contentType}"`
} }
export const serve: HttpAdapter = async (server: HttpServer) => { export const fastifyServe: HttpAdapter = async (server: HttpServer) => {
const fastify = Fastify({ const fastify = Fastify({
logger: false, logger: false,
serverFactory: server.serverFactory, serverFactory: server.serverFactory,
@ -32,7 +31,7 @@ export const serve: HttpAdapter = async (server: HttpServer) => {
fastify.addHook('onResponse', (request: FastifyRequest, reply: FastifyReply) => { fastify.addHook('onResponse', (request: FastifyRequest, reply: FastifyReply) => {
if (reply.statusCode >= 400) { if (reply.statusCode >= 400) {
server.log?.warn(formatCombinedLog(request, reply)) server.log?.warn(formatCombinedLog(request, reply))
} else if (server.debug) { } else if (server.config.debug) {
server.log?.debug(formatCombinedLog(request, reply)) server.log?.debug(formatCombinedLog(request, reply))
} }
}) })
@ -59,7 +58,7 @@ export const serve: HttpAdapter = async (server: HttpServer) => {
adaptResponseToReply(server.onMetrics(), reply) adaptResponseToReply(server.onMetrics(), reply)
}) })
await fastify.listen({ port: server.port, host: '::' }) await fastify.listen({ port: server.config.port, host: '::' })
return { return {
shutdown() { shutdown() {

View file

@ -0,0 +1,2 @@
export * from './api'
export * from './fastify'

View file

@ -1,16 +0,0 @@
import type { HttpResponse } from './adapters/http/api'
import type { Metric } from './metrics'
import type { Logger } from 'homebridge'
import type { RequestListener, Server } from 'http'
export interface HttpServer {
port: number
debug: boolean
log?: Logger
serverFactory?: (requestListener: RequestListener) => Server
onRequest(): HttpResponse | undefined
onMetrics(): HttpResponse
onNotFound(): HttpResponse
onError(error: unknown): HttpResponse
updateMetrics(metrics: Metric[]): void
}

View file

@ -1,4 +1,4 @@
import type { Accessory, Device, Service } from './boundaries/hap' import type { Accessory, Device, Service } from './boundaries'
import { Uuids } from './generated/services' import { Uuids } from './generated/services'
import { assertTypeExhausted, isType } from './std' import { assertTypeExhausted, isType } from './std'

View file

@ -1,9 +1,8 @@
import type { API, IndependentPlatformPlugin, Logger, PlatformConfig } from 'homebridge' import type { API, IndependentPlatformPlugin, Logger, PlatformConfig } from 'homebridge'
import { aggregate } from './metrics' import { aggregate } from './metrics'
import { discover } from './adapters/discovery/hap_node_js_client' import { hapNodeJsClientDiscover as discover } from './adapters/discovery'
import { serve } from './adapters/http/fastify' import { type HttpServerController, fastifyServe as serve } from './adapters/http'
import type { HttpServerController } from './adapters/http/api'
import { PrometheusServer } from './prometheus' import { PrometheusServer } from './prometheus'
import { type Config, ConfigBoundary, checkBoundary } from './boundaries' import { type Config, ConfigBoundary, checkBoundary } from './boundaries'
@ -28,7 +27,7 @@ export class PrometheusExporterPlatform implements IndependentPlatformPlugin {
this.log.debug('Starting Prometheus HTTP server on port %d', this.config.port) this.log.debug('Starting Prometheus HTTP server on port %d', this.config.port)
this.httpServer = new PrometheusServer(this.config.port, this.log, this.config.debug, this.config.prefix) this.httpServer = new PrometheusServer(this.config, this.log)
serve(this.httpServer) serve(this.httpServer)
.then((httpServerController) => { .then((httpServerController) => {
this.log.debug('HTTP server started on port %d', this.config.port) this.log.debug('HTTP server started on port %d', this.config.port)
@ -46,14 +45,7 @@ export class PrometheusExporterPlatform implements IndependentPlatformPlugin {
private startHapDiscovery(): void { private startHapDiscovery(): void {
this.log.debug('Starting HAP discovery') this.log.debug('Starting HAP discovery')
discover({ discover({ log: this.log, config: this.config })
logger: this.log,
refreshInterval: this.config.refresh_interval,
discoveryTimeout: this.config.discovery_timeout,
requestTimeout: this.config.request_timeout,
pin: this.config.pin,
debug: this.config.debug,
})
.then((devices) => { .then((devices) => {
const metrics = aggregate(devices, new Date()) const metrics = aggregate(devices, new Date())
this.httpServer.updateMetrics(metrics) this.httpServer.updateMetrics(metrics)

View file

@ -1,7 +1,6 @@
import type { Metric } from './metrics' import type { Metric } from './metrics'
import type { Logger } from 'homebridge' import type { Logger } from 'homebridge'
import type { HttpResponse } from './adapters/http/api' import type { HttpConfig, HttpResponse, HttpServer } from './adapters/http'
import type { HttpServer } from './http'
export class MetricsRenderer { export class MetricsRenderer {
constructor(private readonly prefix: string) {} constructor(private readonly prefix: string) {}
@ -44,12 +43,7 @@ export class PrometheusServer implements HttpServer {
private metricsInitialized = false private metricsInitialized = false
private metrics: Metric[] = [] private metrics: Metric[] = []
constructor( constructor(public readonly config: HttpConfig, public readonly log: Logger | undefined = undefined) {}
public readonly port: number,
public readonly log: Logger | undefined,
public readonly debug: boolean,
private readonly prefix: string,
) {}
onRequest(): HttpResponse | undefined { onRequest(): HttpResponse | undefined {
if (!this.metricsInitialized) { if (!this.metricsInitialized) {
@ -62,7 +56,7 @@ export class PrometheusServer implements HttpServer {
} }
onMetrics(): HttpResponse { onMetrics(): HttpResponse {
const renderer = new MetricsRenderer(this.prefix) const renderer = new MetricsRenderer(this.config.prefix)
const metrics = this.metrics.map((metric) => renderer.render(metric)).join('\n') const metrics = this.metrics.map((metric) => renderer.render(metric)).join('\n')
return { return {

View file

@ -1,9 +1,8 @@
import { describe, test } from '@jest/globals' import { describe, test } from '@jest/globals'
import request from 'supertest' import request from 'supertest'
import { PrometheusServer } from '../../../src/prometheus' import { PrometheusServer } from '../../../src/prometheus'
import { serve } from '../../../src/adapters/http/fastify' import { type HttpServer, fastifyServe } from '../../../src/adapters/http'
import { type Server, createServer } from 'http' import { type Server, createServer } from 'http'
import type { HttpServer } from '../../../src/http'
import { Metric } from '../../../src/metrics' import { Metric } from '../../../src/metrics'
class TestablePrometheusServer extends PrometheusServer { class TestablePrometheusServer extends PrometheusServer {
@ -12,9 +11,9 @@ class TestablePrometheusServer extends PrometheusServer {
function createTestServer(): { http: Server; prometheus: HttpServer } { function createTestServer(): { http: Server; prometheus: HttpServer } {
const http = createServer() const http = createServer()
const prometheus = new TestablePrometheusServer(0, undefined, false, 'homebridge') const prometheus = new TestablePrometheusServer({ port: 0, debug: false, prefix: 'homebridge' })
prometheus.serverFactory = (handler) => http.on('request', handler) prometheus.serverFactory = (handler) => http.on('request', handler)
serve(prometheus).catch((err: Error) => { fastifyServe(prometheus).catch((err: Error) => {
if (!('code' in err) || (err as unknown as { code: unknown }).code !== 'ERR_SERVER_ALREADY_LISTEN') { if (!('code' in err) || (err as unknown as { code: unknown }).code !== 'ERR_SERVER_ALREADY_LISTEN') {
console.debug(err) console.debug(err)
} }