@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.
- package/dist/index.d.ts +28 -80
- package/dist/index.js +107 -106
- package/package.json +19 -10
package/dist/index.d.ts
CHANGED
|
@@ -1,84 +1,32 @@
|
|
|
1
|
-
import {
|
|
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
|
|
7
|
-
|
|
8
|
-
interface Builder {
|
|
3
|
+
type Phase = 'start' | 'stop';
|
|
4
|
+
interface Block {
|
|
9
5
|
name: string;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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:
|
|
19
|
-
|
|
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 {
|
|
32
|
+
export { App, type Block, type ErrorHandler, type LoadEnvOptions, type Phase, loadEnv };
|
package/dist/index.js
CHANGED
|
@@ -1,123 +1,124 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
52
|
+
async stop() {
|
|
53
|
+
await this.stopAll("manual");
|
|
46
54
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
61
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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/
|
|
90
|
-
import {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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.
|
|
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": "
|
|
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": "
|
|
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
|
+
}
|