diff options
Diffstat (limited to 'deno/base')
-rw-r--r-- | deno/base/config.ts | 93 | ||||
-rw-r--r-- | deno/base/cron.ts | 43 | ||||
-rw-r--r-- | deno/base/deno.json | 10 | ||||
-rw-r--r-- | deno/base/lib.ts | 10 | ||||
-rw-r--r-- | deno/base/log.ts | 60 |
5 files changed, 216 insertions, 0 deletions
diff --git a/deno/base/config.ts b/deno/base/config.ts new file mode 100644 index 0000000..a5f5d86 --- /dev/null +++ b/deno/base/config.ts @@ -0,0 +1,93 @@ +import { camelCaseToKebabCase } from "./lib.ts"; + +export interface ConfigDefinitionItem { + readonly description: string; + readonly default?: string; + readonly secret?: boolean; +} + +interface ConfigMapItem extends ConfigDefinitionItem { + readonly env: string; + value?: string; +} + +export type ConfigDefinition<K extends string = string> = Record< + K, + ConfigDefinitionItem +>; +type ConfigMap<K extends string = string> = Record<K, ConfigMapItem>; + +export class ConfigProvider<K extends string> { + readonly #prefix: string; + readonly #map: ConfigMap<K>; + + constructor(prefix: string, ...definitions: Partial<ConfigDefinition<K>>[]) { + this.#prefix = prefix; + + const map: ConfigMap = {}; + for (const definition of definitions) { + for (const [key, def] of Object.entries(definition as ConfigDefinition)) { + map[key] = { + ...def, + env: `${this.#prefix}-${camelCaseToKebabCase(key as string)}` + .replaceAll("-", "_") + .toUpperCase(), + }; + } + } + this.#map = map as ConfigMap<K>; + } + + resolveFromEnv(options?: { keys?: K[] }) { + const keys = options?.keys ?? Object.keys(this.#map); + for (const key of keys) { + const { env, description, default: _default } = this.#map[key as K]; + const value = Deno.env.get(env) ?? _default; + if (value == null) { + throw new Error(`Required env ${env} (${description}) is not set.`); + } + this.#map[key as K].value = value; + } + } + + get(key: K): string { + if (!(key in this.#map)) { + throw new Error(`Unknown config key ${key as string}.`); + } + if (this.#map[key].value == null) { + this.resolveFromEnv({ keys: [key] }); + } + return this.#map[key].value!; + } + + set(key: K, value: string) { + if (!(key in this.#map)) { + throw new Error(`Unknown config key ${key as string}.`); + } + this.#map[key].value = value; + } + + getInt(key: K): number { + return Number(this.get(key)); + } + + getList(key: K, separator: string = ","): string[] { + const value = this.get(key); + if (value.length === 0) return []; + return value.split(separator); + } + + [Symbol.for("Deno.customInspect")]() { + const getValueString = (item: ConfigMapItem): string => { + if (item.value == null) return "(unresolved)"; + if (item.secret === true) return "***"; + return item.value; + }; + + return Object.entries(this.#map as ConfigMap) + .map( + ([key, item]) => `${key} [env: ${item.env}]: ${getValueString(item)}`, + ) + .join("\n"); + } +} diff --git a/deno/base/cron.ts b/deno/base/cron.ts new file mode 100644 index 0000000..bf0a0be --- /dev/null +++ b/deno/base/cron.ts @@ -0,0 +1,43 @@ +export type CronCallback = (task: CronTask) => Promise<void>; + +export interface CronTaskConfig { + readonly name: string; + readonly interval: number; + readonly callback: CronCallback; + readonly startNow?: boolean; +} + +export class CronTask { + #timerTag: number | null = null; + + constructor(public readonly config: CronTaskConfig) { + if (config.interval <= 0) { + throw new Error("Cron task interval must be positive."); + } + + if (config.startNow === true) { + this.start(); + } + } + + get running(): boolean { + return this.#timerTag != null; + } + + start() { + if (this.#timerTag == null) { + this.#timerTag = setInterval( + this.config.callback, + this.config.interval, + this, + ); + } + } + + stop() { + if (this.#timerTag != null) { + clearInterval(this.#timerTag); + this.#timerTag = null; + } + } +} diff --git a/deno/base/deno.json b/deno/base/deno.json new file mode 100644 index 0000000..dabc02a --- /dev/null +++ b/deno/base/deno.json @@ -0,0 +1,10 @@ +{ + "name": "@crupest/base", + "version": "0.1.0", + "exports": { + ".": "./lib.ts", + "./config": "./config.ts", + "./cron": "./cron.ts", + "./log": "./log.ts" + } +} diff --git a/deno/base/lib.ts b/deno/base/lib.ts new file mode 100644 index 0000000..a5e4a6a --- /dev/null +++ b/deno/base/lib.ts @@ -0,0 +1,10 @@ +export function camelCaseToKebabCase(str: string): string { + return str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase()); +} + +export function toFileNameString(date: Date, dateOnly?: boolean): string { + const str = date.toISOString(); + return dateOnly === true + ? str.slice(0, str.indexOf("T")) + : str.replaceAll(/:|\./g, "-"); +} diff --git a/deno/base/log.ts b/deno/base/log.ts new file mode 100644 index 0000000..940f569 --- /dev/null +++ b/deno/base/log.ts @@ -0,0 +1,60 @@ +import { join } from "@std/path"; + +import { toFileNameString } from "./lib.ts"; + +export interface ExternalLogStream extends Disposable { + stream: WritableStream; +} + +export class LogFileProvider { + #directory: string; + + constructor(directory: string) { + this.#directory = directory; + Deno.mkdirSync(directory, { recursive: true }); + } + + async createExternalLogStream( + name: string, + options?: { + noTime?: boolean; + }, + ): Promise<ExternalLogStream> { + if (name.includes("/")) { + throw new Error(`External log stream's name (${name}) contains '/'.`); + } + + const logPath = join( + this.#directory, + options?.noTime === true + ? name + : `${name}-${toFileNameString(new Date())}`, + ); + + const file = await Deno.open(logPath, { + read: false, + write: true, + append: true, + create: true, + }); + return { + stream: file.writable, + [Symbol.dispose]: file[Symbol.dispose].bind(file), + }; + } + + async createExternalLogStreamsForProgram( + program: string, + ): Promise<{ stdout: WritableStream; stderr: WritableStream } & Disposable> { + const stdout = await this.createExternalLogStream(`${program}-stdout`); + const stderr = await this.createExternalLogStream(`${program}-stderr`); + return { + stdout: stdout.stream, + stderr: stderr.stream, + [Symbol.dispose]: () => { + stdout[Symbol.dispose](); + stderr[Symbol.dispose](); + }, + }; + } +} |