@aromix/core 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +28 -80
  2. package/dist/index.js +107 -106
  3. package/package.json +19 -10
package/dist/index.d.ts CHANGED
@@ -1,84 +1,32 @@
1
- import { AnySchema } from '@aromix/validator';
2
- import { Server } from 'http';
3
- import { Db, MongoClient } from 'mongodb';
4
- import { RedisClientType, RedisClusterType, RedisClientPoolType, RedisSentinelType } from 'redis';
1
+ import { AxSchemaShape, AxObjectSchema } from '@aromix/validator';
5
2
 
6
- type MaybePromise<T> = T | Promise<T>
7
-
8
- interface Builder {
3
+ type Phase = 'start' | 'stop';
4
+ interface Block {
9
5
  name: string;
10
- onRegister?(state: Record<string, any>): MaybePromise<void>;
11
- onListen?(server: Server): MaybePromise<void>;
12
- onShutdown?(): MaybePromise<void>;
13
- }
14
- declare function Builder(def: Builder): Builder;
15
-
16
- interface ValidateEnvConfig<Schema extends Record<string, AnySchema>> {
6
+ start(): void | Promise<void>;
7
+ stop?(): void | Promise<void>;
8
+ error?(err: Error, phase: Phase): void | Promise<void>;
9
+ }
10
+ type ErrorHandler = (err: Error, phase: Phase | 'runtime') => void | Promise<void>;
11
+
12
+ declare class App {
13
+ private isStarted;
14
+ private isStopping;
15
+ private blocks;
16
+ private errorHandlers;
17
+ use(block: Block): void;
18
+ onError(handler: ErrorHandler): this;
19
+ start(): Promise<void>;
20
+ stop(): Promise<void>;
21
+ private notifyError;
22
+ private stopAll;
23
+ private gracefulShutdown;
24
+ }
25
+
26
+ type LoadEnvOptions<Shape extends AxSchemaShape> = Partial<{
17
27
  path: string;
18
- schema: Schema;
19
- onError?(err: unknown): void;
20
- }
21
- declare function ValidateEnv<const Schema extends Record<string, AnySchema>>(config: ValidateEnvConfig<Schema>): readonly [Builder, <Key extends keyof Schema>(key: Key, fallback?: Schema[Key]["$base"]) => Schema[Key]["$base"]];
22
-
23
- interface EffectDef {
24
- name: string;
25
- run(): void;
26
- }
27
- declare function effect(def: EffectDef): EffectDef;
28
-
29
- interface GuardDef {
30
- name: string;
31
- run(): void;
32
- }
33
- declare function guard(def: GuardDef): GuardDef;
34
-
35
- declare class MongoDatabase {
36
- db: Db;
37
- constructor();
38
- /** internal attach */
39
- attach(db: Db): void;
40
- entity<Schema extends AnySchema>(input: MongoEntityInput<Schema>): void;
41
- }
42
-
43
- type ClusterResult<T extends readonly string[]> = {
44
- builder: Builder;
45
- client: MongoClient;
46
- } & {
47
- [K in T[number]]: MongoDatabase;
48
- };
49
- interface MongoClusterInput<Databases extends readonly string[]> {
50
- name: string;
51
- uri: string;
52
- databases: Databases;
53
- onConnect?(client: MongoClient): MaybePromise<void>;
54
- onDisconnect?(client: MongoClient): MaybePromise<void>;
55
- onError?(err: unknown): MaybePromise<void>;
56
- }
57
- interface MongoEntityInput<Schema extends AnySchema> {
58
- name: string;
59
- model: Schema;
60
- }
61
-
62
- declare function MongoCluster<const Databases extends readonly string[]>(options: MongoClusterInput<Databases>): ClusterResult<Databases>;
63
-
64
- type RedisAdapter = RedisClientType | RedisClusterType | RedisClientPoolType | RedisSentinelType;
65
- interface RedisInput {
66
- name: string;
67
- client(): RedisAdapter;
68
- onConnect?(client: RedisAdapter): MaybePromise<void>;
69
- onDisconnect?(client: RedisAdapter): MaybePromise<void>;
70
- onError?(err: unknown): void;
71
- }
72
- declare class Redis {
73
- constructor(input: RedisInput);
74
- }
75
-
76
- declare class Application {
77
- private state;
78
- private builders;
79
- register(builder: Builder): void;
80
- listen(port: number): Promise<void>;
81
- }
82
- declare function bootstrap(): Application;
28
+ schema: AxObjectSchema<Shape>;
29
+ }>;
30
+ declare function loadEnv<Shape extends AxSchemaShape>(options: LoadEnvOptions<Shape>): Block;
83
31
 
84
- export { Builder, type ClusterResult, type EffectDef, type GuardDef, type MaybePromise, MongoCluster, type MongoClusterInput, MongoDatabase, type MongoEntityInput, Redis, type RedisAdapter, type RedisInput, ValidateEnv, type ValidateEnvConfig, bootstrap, effect, guard };
32
+ export { App, type Block, type ErrorHandler, type LoadEnvOptions, type Phase, loadEnv };
package/dist/index.js CHANGED
@@ -1,123 +1,124 @@
1
- // src/server/builder.ts
2
- function Builder(def) {
3
- return def;
4
- }
5
-
6
- // src/common/validate.env.ts
7
- import { loadEnvFile } from "process";
8
- function ValidateEnv(config) {
9
- const builder = Builder({
10
- name: "validate.env",
11
- onRegister(state) {
12
- loadEnvFile(config.path);
13
- console.log(state);
14
- console.log(process.env);
1
+ // src/app/builder.ts
2
+ var App = class {
3
+ isStarted = false;
4
+ isStopping = false;
5
+ blocks = [];
6
+ errorHandlers = [];
7
+ use(block) {
8
+ if (this.isStarted) {
9
+ throw new Error(`[Aromix] Unable to register "${block.name}": process already started.`);
10
+ }
11
+ if (this.blocks.some((b) => b.name === block.name)) {
12
+ throw new Error(`[Aromix] Block with name "${block.name}" is already registered.`);
15
13
  }
16
- });
17
- function env(key, fallback) {
18
- return process.env[key] ?? fallback;
14
+ this.blocks.push(block);
15
+ console.debug("[Aromix] Block registered", { block: block.name, phase: "register" });
19
16
  }
20
- return [builder, env];
21
- }
22
-
23
- // src/layer/effect.ts
24
- function effect(def) {
25
- return def;
26
- }
27
-
28
- // src/layer/guard.ts
29
- function guard(def) {
30
- return def;
31
- }
32
-
33
- // src/mongo/cluster.ts
34
- import { MongoClient } from "mongodb";
35
-
36
- // src/mongo/database.ts
37
- var MongoDatabase = class {
38
- db;
39
- constructor() {
17
+ onError(handler) {
18
+ this.errorHandlers.push(handler);
19
+ return this;
40
20
  }
41
- /** internal attach */
42
- attach(db) {
43
- this.db = db;
21
+ async start() {
22
+ if (this.isStarted) {
23
+ throw new Error("[Aromix] Process has already been started.");
24
+ }
25
+ this.isStarted = true;
26
+ console.info("[Aromix] Starting application", { totalBlocks: this.blocks.length });
27
+ for (const block of this.blocks) {
28
+ const startedAt = Date.now();
29
+ try {
30
+ await block.start();
31
+ console.log("[Aromix] Block started", { block: block.name, phase: "start", durationMs: Date.now() - startedAt });
32
+ } catch (err) {
33
+ console.error("[Aromix] Fatal: block failed to start. Rolling back...", err, { block: block.name, phase: "start" });
34
+ await this.notifyError(err, "start", block);
35
+ await this.stopAll("start-failure");
36
+ process.exit(1);
37
+ }
38
+ }
39
+ process.on("uncaughtException", (err) => {
40
+ console.error("[Aromix] Uncaught exception. Shutting down...", err);
41
+ this.notifyError(err, "runtime").finally(() => this.gracefulShutdown("uncaughtException"));
42
+ });
43
+ process.on("unhandledRejection", (reason) => {
44
+ const err = reason instanceof Error ? reason : new Error(String(reason));
45
+ console.error("[Aromix] Unhandled rejection. Shutting down...", err);
46
+ this.notifyError(err, "runtime").finally(() => this.gracefulShutdown("unhandledRejection"));
47
+ });
48
+ process.once("SIGINT", () => this.gracefulShutdown("SIGINT"));
49
+ process.once("SIGTERM", () => this.gracefulShutdown("SIGTERM"));
50
+ console.log("[Aromix] Application started successfully", { totalBlocks: this.blocks.length });
44
51
  }
45
- entity(input) {
52
+ async stop() {
53
+ await this.stopAll("manual");
46
54
  }
47
- };
48
-
49
- // src/mongo/cluster.ts
50
- function MongoCluster(options) {
51
- const client = new MongoClient(options.uri);
52
- const databases = {};
53
- for (const name of options.databases) {
54
- databases[name] = new MongoDatabase();
55
+ async notifyError(err, phase, block) {
56
+ const error = err instanceof Error ? err : new Error(String(err));
57
+ if (block?.error && (phase === "start" || phase === "stop")) {
58
+ try {
59
+ await block.error(error, phase);
60
+ } catch (handlerErr) {
61
+ console.error(`[Aromix] Block "${block.name}" error handler itself threw`, handlerErr, { block: block.name, phase });
62
+ }
63
+ }
64
+ for (const handler of this.errorHandlers) {
65
+ try {
66
+ await handler(error, phase);
67
+ } catch (handlerErr) {
68
+ console.error("[Aromix] Global error handler threw", handlerErr, { phase });
69
+ }
70
+ }
55
71
  }
56
- const builder = Builder({
57
- name: options.name,
58
- async onListen() {
72
+ async stopAll(reason) {
73
+ if (this.isStopping) return true;
74
+ this.isStopping = true;
75
+ console.info("[Aromix] Stopping all blocks", { phase: "stop", signal: reason, totalBlocks: this.blocks.length });
76
+ let allStoppedCleanly = true;
77
+ for (let i = this.blocks.length - 1; i >= 0; i--) {
78
+ const block = this.blocks[i];
79
+ const startedAt = Date.now();
59
80
  try {
60
- await client.connect();
61
- for (const name of options.databases) {
62
- const db = client.db(name);
63
- databases[name].attach(db);
64
- }
65
- await options.onConnect?.(client);
81
+ await block.stop?.();
82
+ console.log("[Aromix] Block stopped", { block: block.name, phase: "stop", durationMs: Date.now() - startedAt });
66
83
  } catch (err) {
67
- await options.onError?.(err);
68
- throw err;
84
+ allStoppedCleanly = false;
85
+ console.error("[Aromix] Block failed to stop", err, { block: block.name, phase: "stop" });
86
+ await this.notifyError(err, "stop", block);
69
87
  }
70
- },
71
- async onShutdown() {
72
- await options.onDisconnect?.(client);
73
- await client.close();
74
88
  }
75
- });
76
- return {
77
- client,
78
- builder,
79
- ...databases
80
- };
81
- }
82
-
83
- // src/redis/redis.ts
84
- var Redis = class {
85
- constructor(input) {
89
+ return allStoppedCleanly;
90
+ }
91
+ async gracefulShutdown(signal) {
92
+ console.info(`[Aromix] Received ${signal}. Starting graceful shutdown...`, { signal });
93
+ const clean = await this.stopAll(signal);
94
+ process.exit(clean ? 0 : 1);
86
95
  }
87
96
  };
88
97
 
89
- // src/server/application.ts
90
- import { createServer } from "http";
91
- var Application = class {
92
- state = {};
93
- builders = [];
94
- register(builder) {
95
- this.builders.push(builder);
96
- builder.onRegister?.(this.state);
97
- }
98
- async listen(port) {
99
- const server = createServer();
100
- for (const builder of this.builders) {
101
- await builder.onListen?.(server);
102
- }
103
- server.listen(port);
104
- server.on("close", async () => {
105
- for (const builder of this.builders) {
106
- await builder.onShutdown?.();
98
+ // src/common/load.env.ts
99
+ import { resolve } from "path";
100
+ import { existsSync } from "fs";
101
+ function loadEnv(options) {
102
+ const path = resolve(options.path ?? ".env");
103
+ return {
104
+ name: "Load Env",
105
+ async start() {
106
+ if (!existsSync(path)) {
107
+ throw new Error(`[Load Env] Environment file not found: ${path}`);
107
108
  }
108
- });
109
- }
110
- };
111
- function bootstrap() {
112
- return new Application();
109
+ process.loadEnvFile(path);
110
+ if (options.schema) {
111
+ const [result, issues] = options.schema.parseBase(process.env);
112
+ if (issues) {
113
+ throw new Error("[Load Env] Schema Validation Failed", {
114
+ cause: issues
115
+ });
116
+ }
117
+ }
118
+ }
119
+ };
113
120
  }
114
121
  export {
115
- Builder,
116
- MongoCluster,
117
- MongoDatabase,
118
- Redis,
119
- ValidateEnv,
120
- bootstrap,
121
- effect,
122
- guard
122
+ App,
123
+ loadEnv
123
124
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aromix/core",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "The Core Package For Aromix",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,23 +34,22 @@
34
34
  "access": "public",
35
35
  "registry": "https://registry.npmjs.org"
36
36
  },
37
- "scripts": {
38
- "build": "tsup",
39
- "dev": "tsup --watch",
40
- "typecheck": "tsc --noEmit"
41
- },
42
37
  "peerDependencies": {
43
- "@aromix/validator": "^0.3.0",
38
+ "@aromix/validator": "0.5.5",
44
39
  "mongodb": "^7.3.0",
45
40
  "redis": "^6.0.1"
46
41
  },
47
42
  "devDependencies": {
48
- "@aromix/validator": "^0.3.0",
43
+ "@aromix/validator": "0.5.5",
44
+ "@biomejs/biome": "^1.9.4",
49
45
  "@types/node": "^22.10.2",
46
+ "beachball": "^2.65.5",
50
47
  "mongodb": "^7.3.0",
51
48
  "redis": "^6.0.1",
49
+ "rimraf": "^6.1.3",
52
50
  "tsup": "^8.3.5",
53
- "typescript": "^5.7.2"
51
+ "typescript": "^5.7.2",
52
+ "vitest": "^4.1.6"
54
53
  },
55
54
  "peerDependenciesMeta": {
56
55
  "mongodb": {
@@ -59,5 +58,15 @@
59
58
  "redis": {
60
59
  "optional": true
61
60
  }
61
+ },
62
+ "scripts": {
63
+ "build": "tsup",
64
+ "dev": "tsup --watch",
65
+ "typecheck": "tsc --noEmit",
66
+ "format": "biome format --write .",
67
+ "test": "vitest run",
68
+ "change": "beachball change",
69
+ "bump": "beachball bump",
70
+ "release": "beachball publish --yes --access public"
62
71
  }
63
- }
72
+ }