diff --git a/src/index.ts b/src/index.ts index 6239d6e..7921f20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import Debug from "debug"; +import { Logger } from "./utils/logger.js"; import { Engine } from "@xo-cash/engine"; import { Config } from "./services/config.js"; @@ -22,7 +22,7 @@ type VendingMachineDeps = { export class VendingMachine { static async from(config: Config) { - const debug = Debug("vending-machine"); + const debug = new Logger("vending-machine"); debug("Config: %O", config); @@ -65,7 +65,7 @@ export class VendingMachine { const httpService = new HTTPService({ routes, config: config.server, - debug, + debug: new Logger("vending-machine"), }); debug("Creating vending machine"); diff --git a/src/routes/items.ts b/src/routes/items.ts index 297acee..7e28eae 100644 --- a/src/routes/items.ts +++ b/src/routes/items.ts @@ -1,4 +1,4 @@ -import type { Debugger as Debug } from "debug"; +import type { Logger } from '../utils/logger.js'; import type { RouteOptions, FastifyRequest, FastifyReply } from "fastify"; import type { Engine } from "@xo-cash/engine"; import type { Database } from "../services/database/database.js"; @@ -8,7 +8,7 @@ import { z } from "zod"; export type ItemsRouteDeps = { database: Database; engine: Engine; - debug: Debug; + debug: Logger; }; export class ItemsRoute { diff --git a/src/routes/orders.ts b/src/routes/orders.ts index ed35f47..05febf8 100644 --- a/src/routes/orders.ts +++ b/src/routes/orders.ts @@ -1,4 +1,4 @@ -import type { Debugger as Debug } from "debug"; +import type { Logger } from '../utils/logger.js'; import type { RouteOptions, FastifyRequest, FastifyReply } from "fastify"; import { z } from "zod"; @@ -13,7 +13,7 @@ export type OrdersRouteDeps = { database: Database; orderPaymentService: OrderPaymentService; syncServerUrl: string; - debug: Debug; + debug: Logger; }; export class OrdersRoute { diff --git a/src/services/database/database.ts b/src/services/database/database.ts index fd9ff5b..cf8619b 100644 --- a/src/services/database/database.ts +++ b/src/services/database/database.ts @@ -1,18 +1,19 @@ -import { type Debugger } from "debug"; +import { Logger } from '../../utils/logger.js'; import DatabaseConstructor from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; import type { Database as DatabaseTables } from "./tables.js"; import { z } from "zod"; -import { debuggerSchema } from "../../types.js"; export const databaseOptionsSchema = z.object({ config: z.object({ path: z.string(), }), - debug: debuggerSchema, + debug: z.custom(Logger.isLogger, { + message: "Expected a Logger instance", + }), }); type DatabaseOptionsInput = z.input; @@ -26,7 +27,7 @@ type DatabaseOptionsInput = z.input; */ export class Database { // Debugger instance used for logging. - private readonly debug: Debugger; + private readonly debug: Logger; // SQLite connection instance. private readonly sqlite: DatabaseConstructor.Database; diff --git a/src/services/http-router.ts b/src/services/http-router.ts index 2d60cb9..25c78f5 100644 --- a/src/services/http-router.ts +++ b/src/services/http-router.ts @@ -1,10 +1,9 @@ -import Debug from "debug"; import { z } from "zod"; import fastify, { type FastifyInstance, type RouteOptions } from "fastify"; import cors from "@fastify/cors"; -import { debuggerSchema } from "../types.js"; +import { Logger } from "../utils/logger.js"; // Interface to add to our route classes so that we can register them. // NOTE: I hate this pattern. But ExpressJS is odd in that it is structured as a singleton that still needs registration. @@ -33,9 +32,12 @@ export const serverConfigSchema = z.object({ }); // Zod schema for the server debug instance. -export const serverDebugSchema = debuggerSchema +export const serverDebugSchema = z + .custom(Logger.isLogger, { + message: "Expected a Logger instance", + }) .optional() - .default(Debug("vending-machine")); + .default(new Logger("vending-machine")); // Zod schema for the HTTP service options. export const HTTPOptions = z.object({ @@ -61,7 +63,7 @@ export class HTTPService { private server: FastifyInstance; // Private properties. - private debug: debug.Debugger; + private debug: Logger; private config: HTTPServiceOptions["config"]; private routes: HTTPServiceOptions["routes"]; diff --git a/src/services/order-invitation-tracker.ts b/src/services/order-invitation-tracker.ts index ca002d2..abf3d88 100644 --- a/src/services/order-invitation-tracker.ts +++ b/src/services/order-invitation-tracker.ts @@ -1,4 +1,4 @@ -import type { Debugger } from "debug"; +import type { Logger } from '../utils/logger.js'; import type { Engine } from "@xo-cash/engine"; import type { XOInvitation, XOInvitationCommit } from "@xo-cash/types"; @@ -11,7 +11,7 @@ export type OrderInvitationTrackerDeps = { database: Database; orderId: string; invitation: XOInvitation; - debug: Debugger; + debug: Logger; }; /** diff --git a/src/services/order-payment-service.ts b/src/services/order-payment-service.ts index 11579c8..1727687 100644 --- a/src/services/order-payment-service.ts +++ b/src/services/order-payment-service.ts @@ -1,11 +1,10 @@ -import type { Debugger } from "debug"; +import type { Logger } from '../utils/logger.js'; import type { Engine } from "@xo-cash/engine"; import { serializeInvitation } from "@xo-cash/engine"; import { vendingMachineTemplate } from "../templates/vending-machine.js"; import type { Config } from "./config.js"; import type { Database } from "./database/database.js"; -import type { ItemsTable } from "./database/tables.js"; import { InvitationSyncClient } from "./invitation-sync-client.js"; import { OrderInvitationTracker } from "./order-invitation-tracker.js"; @@ -40,7 +39,7 @@ export type OrderPaymentServiceDeps = { database: Database; config: Config; syncClient: InvitationSyncClient; - debug: Debugger; + debug: Logger; templateIdentifier: string; trackers: Map; }; @@ -55,7 +54,7 @@ export class OrderPaymentService { engine: Engine; database: Database; config: Config; - debug: Debugger; + debug: Logger; trackers: Map; }): Promise { const { templateIdentifier } = await deps.engine.importTemplate( diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index cc89351..0000000 --- a/src/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type Debugger } from "debug"; -import { z } from "zod"; - -export const isDebugger = (value: unknown): value is Debugger => { - if (typeof value !== "function") return false; - - const candidate = value as Partial; - - return ( - typeof candidate.namespace === "string" && - typeof candidate.extend === "function" && - typeof candidate.enabled === "boolean" - ); -}; - -export const debuggerSchema = z.custom(isDebugger, { - message: "Expected a debug.Debugger instance", -}); diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..b5bdb4c --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,57 @@ +import Debug, { type Debugger } from "debug"; + +type LogHandler = { + (...args: Parameters): void; + extend: (namespace: string) => LogHandler; +} + +/** + * Declares that Logger instances may also be invoked as functions. + */ +export interface Logger { + (...args: unknown[]): void; +} + +export class Logger { + public readonly namespace!: string; + + private readonly handler!: LogHandler; + + public constructor( + namespace: string, + handler: LogHandler = Debug(namespace), + ) { + const logger = ((...args: Parameters): void => { + handler(...args); + }) as Logger; + + /* + * This makes `logger instanceof Logger` true and provides + * access to instance methods such as `extend`. + */ + Object.setPrototypeOf(logger, new.target.prototype); + + Object.defineProperties(logger, { + namespace: { + value: namespace, + enumerable: true, + }, + handler: { + value: handler, + }, + }); + + return logger; + } + + public extend(childNamespace: string): Logger { + return new Logger( + childNamespace, + this.handler.extend(childNamespace), + ); + } + + static isLogger(value: unknown): value is Logger { + return value instanceof Logger; + } +}