aboutsummaryrefslogtreecommitdiff
path: root/deno/base
diff options
context:
space:
mode:
Diffstat (limited to 'deno/base')
-rw-r--r--deno/base/config.ts93
-rw-r--r--deno/base/cron.ts43
-rw-r--r--deno/base/deno.json10
-rw-r--r--deno/base/lib.ts10
-rw-r--r--deno/base/log.ts60
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]();
+ },
+ };
+ }
+}