@aromix/core 0.4.1 → 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 +25 -247
  2. package/dist/index.js +107 -363
  3. package/package.json +27 -14
package/dist/index.d.ts CHANGED
@@ -1,254 +1,32 @@
1
- import { AnySchema } from '@aromix/validator';
2
- import { Db, InsertOneOptions, InsertOneResult, BulkWriteOptions, InsertManyResult, Filter, FindOptions, UpdateFilter, UpdateOptions, UpdateResult, ReplaceOptions, DeleteOptions, DeleteResult, FindOneAndUpdateOptions, FindOneAndDeleteOptions, FindOneAndReplaceOptions, CountDocumentsOptions, DistinctOptions, AggregateOptions, AggregationCursor, BulkWriteResult, CreateIndexesOptions, DropIndexesOptions } from 'mongodb';
3
- import * as http from 'http';
4
- import { IncomingMessage, ServerResponse } from 'http';
1
+ import { AxSchemaShape, AxObjectSchema } from '@aromix/validator';
5
2
 
6
- interface KvEntityUserInput<Schema extends AnySchema> {
3
+ type Phase = 'start' | 'stop';
4
+ interface Block {
7
5
  name: string;
8
- model: Schema;
6
+ start(): void | Promise<void>;
7
+ stop?(): void | Promise<void>;
8
+ error?(err: Error, phase: Phase): void | Promise<void>;
9
9
  }
10
- declare class KvEntity<Schema extends AnySchema> {
11
- readonly state: KvEntityUserInput<Schema>;
12
- private adapter;
13
- constructor(userProvided: KvEntityUserInput<Schema>, internal: EntityBuilder.KvAdapter);
14
- get(key: string): Promise<Schema['$select']>;
15
- set(key: string, value: Schema['$insert']): Promise<void>;
16
- delete(key: string): Promise<void>;
17
- has(key: string): Promise<boolean>;
18
- }
19
-
20
- interface MongoEntityUserInput<Schema extends AnySchema> {
21
- name: string;
22
- model: Schema;
23
- }
24
- declare class MongoEntity<Schema extends AnySchema> {
25
- readonly states: MongoEntityUserInput<Schema>;
26
- private readonly collection;
27
- constructor(userInput: MongoEntityUserInput<Schema>, db: Db);
28
- insertOne(doc: Schema['$insert'], options?: InsertOneOptions): Promise<InsertOneResult>;
29
- insertMany(docs: Schema['$insert'][], options?: BulkWriteOptions): Promise<InsertManyResult>;
30
- findOne(filter: Filter<Schema['$select']>, options?: FindOptions): Promise<Schema['$select'] | null>;
31
- find(filter: Filter<Schema['$select']>, options?: FindOptions): Promise<Schema['$select'][]>;
32
- updateOne(filter: Filter<Schema['$select']>, update: UpdateFilter<Schema['$update']>, options?: UpdateOptions): Promise<UpdateResult>;
33
- updateMany(filter: Filter<Schema['$select']>, update: UpdateFilter<Schema['$update']>, options?: UpdateOptions): Promise<UpdateResult>;
34
- replaceOne(filter: Filter<Schema['$select']>, replacement: Schema['$select'], options?: ReplaceOptions): Promise<UpdateResult>;
35
- deleteOne(filter: Filter<Schema['$select']>, options?: DeleteOptions): Promise<DeleteResult>;
36
- deleteMany(filter: Filter<Schema['$select']>, options?: DeleteOptions): Promise<DeleteResult>;
37
- findOneAndUpdate(filter: Filter<Schema['$select']>, update: UpdateFilter<Schema['$update']>, options?: FindOneAndUpdateOptions): Promise<Schema['$select'] | null>;
38
- findOneAndDelete(filter: Filter<Schema['$select']>, options?: FindOneAndDeleteOptions): Promise<Schema['$select'] | null>;
39
- findOneAndReplace(filter: Filter<Schema['$select']>, replacement: Schema['$select'], options?: FindOneAndReplaceOptions): Promise<Schema['$select'] | null>;
40
- countDocuments(filter: Filter<Schema['$select']>, options?: CountDocumentsOptions): Promise<number>;
41
- estimatedDocumentCount(): Promise<number>;
42
- distinct<Field extends keyof Schema['$select'] & string>(field: Field, filter: Filter<Schema['$select']>, options?: DistinctOptions): Promise<Array<Schema['$select'][Field]>>;
43
- aggregate(pipeline: any[], options?: AggregateOptions): AggregationCursor;
44
- bulkWrite(operations: any[], options?: BulkWriteOptions): Promise<BulkWriteResult>;
45
- createIndex(indexSpec: any, options?: CreateIndexesOptions): Promise<string>;
46
- dropIndex(indexName: string, options?: DropIndexesOptions): Promise<void>;
47
- }
48
-
49
- declare namespace EntityBuilder {
50
- interface KvAdapter {
51
- get(key: string): Promise<unknown>;
52
- set(key: string, value: unknown): Promise<void>;
53
- delete(key: string): Promise<void>;
54
- has(key: string): Promise<boolean>;
55
- }
56
- interface KvInput {
57
- transport: 'http';
58
- adapter(): KvAdapter;
59
- }
60
- function kv(builderInput: KvInput): {
61
- entity<Schema extends AnySchema>(entityInput: KvEntityUserInput<Schema>): KvEntity<Schema>;
62
- };
63
- interface MongoInput {
64
- adapter(): Db;
65
- transport: 'http';
66
- }
67
- function mongo(builderInput: MongoInput): {
68
- entity<Schema extends AnySchema>(entityInput: MongoEntityUserInput<Schema>): MongoEntity<Schema>;
69
- };
70
- }
71
-
72
- interface ComposeInput {
73
- entities: Array<MongoEntity<AnySchema> | KvEntity<AnySchema>>;
74
- }
75
- declare function compose(input: ComposeInput): {
76
- readonly descriptor: {
77
- entities: any[];
78
- };
79
- dispatch(route: string, payload: unknown): Promise<any>;
80
- };
10
+ type ErrorHandler = (err: Error, phase: Phase | 'runtime') => void | Promise<void>;
81
11
 
82
- declare class Descriptor {
83
- private isExcluded;
84
- private parseSchema;
85
- kv(entity: KvEntity<AnySchema>): {
86
- name: string;
87
- platform: string;
88
- methods: ({
89
- name: string;
90
- route: string;
91
- input: {
92
- type: string;
93
- shape?: undefined;
94
- };
95
- output: any;
96
- } | {
97
- name: string;
98
- route: string;
99
- input: {
100
- type: string;
101
- shape: {
102
- key: {
103
- type: string;
104
- };
105
- value: any;
106
- };
107
- };
108
- output: {
109
- type: string;
110
- };
111
- })[];
112
- };
113
- private buildMongoMethod;
114
- mongo(entity: MongoEntity<AnySchema>): {
115
- name: string;
116
- platform: string;
117
- methods: {
118
- name: string;
119
- route: string;
120
- input: any;
121
- output: {
122
- type: string;
123
- shape: {
124
- acknowledged: {
125
- type: string;
126
- };
127
- matchedCount: {
128
- type: string;
129
- };
130
- modifiedCount: {
131
- type: string;
132
- };
133
- upsertedCount: {
134
- type: string;
135
- };
136
- upsertedId: {
137
- type: string;
138
- };
139
- };
140
- } | {
141
- type: string;
142
- shape: {
143
- acknowledged: {
144
- type: string;
145
- };
146
- insertedId: {
147
- type: string;
148
- };
149
- insertedCount?: undefined;
150
- insertedIds?: undefined;
151
- matchedCount?: undefined;
152
- modifiedCount?: undefined;
153
- upsertedCount?: undefined;
154
- upsertedId?: undefined;
155
- deletedCount?: undefined;
156
- };
157
- items?: undefined;
158
- element?: undefined;
159
- } | {
160
- type: string;
161
- shape: {
162
- acknowledged: {
163
- type: string;
164
- };
165
- insertedCount: {
166
- type: string;
167
- };
168
- insertedIds: {
169
- type: string;
170
- };
171
- insertedId?: undefined;
172
- matchedCount?: undefined;
173
- modifiedCount?: undefined;
174
- upsertedCount?: undefined;
175
- upsertedId?: undefined;
176
- deletedCount?: undefined;
177
- };
178
- items?: undefined;
179
- element?: undefined;
180
- } | {
181
- type: string;
182
- items: any[];
183
- shape?: undefined;
184
- element?: undefined;
185
- } | {
186
- type: string;
187
- element: any;
188
- shape?: undefined;
189
- items?: undefined;
190
- } | {
191
- type: string;
192
- shape: {
193
- acknowledged: {
194
- type: string;
195
- };
196
- matchedCount: {
197
- type: string;
198
- };
199
- modifiedCount: {
200
- type: string;
201
- };
202
- upsertedCount: {
203
- type: string;
204
- };
205
- upsertedId: {
206
- type: string;
207
- };
208
- insertedId?: undefined;
209
- insertedCount?: undefined;
210
- insertedIds?: undefined;
211
- deletedCount?: undefined;
212
- };
213
- items?: undefined;
214
- element?: undefined;
215
- } | {
216
- type: string;
217
- items: any[];
218
- shape?: undefined;
219
- element?: undefined;
220
- } | {
221
- type: string;
222
- shape: {
223
- acknowledged: {
224
- type: string;
225
- };
226
- deletedCount: {
227
- type: string;
228
- };
229
- insertedId?: undefined;
230
- insertedCount?: undefined;
231
- insertedIds?: undefined;
232
- matchedCount?: undefined;
233
- modifiedCount?: undefined;
234
- upsertedCount?: undefined;
235
- upsertedId?: undefined;
236
- };
237
- items?: undefined;
238
- element?: undefined;
239
- } | {
240
- type: string;
241
- shape?: undefined;
242
- items?: undefined;
243
- element?: undefined;
244
- };
245
- }[];
246
- };
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;
247
24
  }
248
25
 
249
- type Composed = ReturnType<typeof compose>;
250
- declare function make(composed: Composed): {
251
- listen(port: number): http.Server<typeof IncomingMessage, typeof ServerResponse>;
252
- };
26
+ type LoadEnvOptions<Shape extends AxSchemaShape> = Partial<{
27
+ path: string;
28
+ schema: AxObjectSchema<Shape>;
29
+ }>;
30
+ declare function loadEnv<Shape extends AxSchemaShape>(options: LoadEnvOptions<Shape>): Block;
253
31
 
254
- export { type ComposeInput, Descriptor, EntityBuilder, KvEntity, type KvEntityUserInput, MongoEntity, type MongoEntityUserInput, compose, make };
32
+ export { App, type Block, type ErrorHandler, type LoadEnvOptions, type Phase, loadEnv };
package/dist/index.js CHANGED
@@ -1,380 +1,124 @@
1
- // src/entity/platforms/kv.ts
2
- var KvEntity = class {
3
- state;
4
- adapter;
5
- constructor(userProvided, internal) {
6
- this.state = userProvided;
7
- this.adapter = internal;
8
- }
9
- async get(key) {
10
- const formattedKey = `${this.state.name}:${key}`;
11
- const raw = await this.adapter.get(formattedKey);
12
- return raw;
13
- }
14
- async set(key, value) {
15
- const formattedKey = `${this.state.name}:${key}`;
16
- await this.adapter.set(formattedKey, value);
17
- }
18
- async delete(key) {
19
- const formattedKey = `${this.state.name}:${key}`;
20
- await this.adapter.delete(formattedKey);
21
- }
22
- async has(key) {
23
- const formattedKey = `${this.state.name}:${key}`;
24
- return this.adapter.has(formattedKey);
25
- }
26
- };
27
-
28
- // src/entity/platforms/mongo.ts
29
- var MongoEntity = class {
30
- states;
31
- collection;
32
- constructor(userInput, db) {
33
- this.states = userInput;
34
- this.collection = db.collection(userInput.name);
35
- }
36
- async insertOne(doc, options) {
37
- return this.collection.insertOne(doc, options);
38
- }
39
- async insertMany(docs, options) {
40
- return this.collection.insertMany(docs, options);
41
- }
42
- async findOne(filter, options) {
43
- return this.collection.findOne(filter, options);
44
- }
45
- async find(filter, options) {
46
- return this.collection.find(filter, options).toArray();
47
- }
48
- async updateOne(filter, update, options) {
49
- return this.collection.updateOne(filter, update, options);
50
- }
51
- async updateMany(filter, update, options) {
52
- return this.collection.updateMany(filter, update, options);
53
- }
54
- async replaceOne(filter, replacement, options) {
55
- return this.collection.replaceOne(filter, replacement, options);
56
- }
57
- async deleteOne(filter, options) {
58
- return this.collection.deleteOne(filter, options);
59
- }
60
- async deleteMany(filter, options) {
61
- return this.collection.deleteMany(filter, options);
62
- }
63
- async findOneAndUpdate(filter, update, options) {
64
- return this.collection.findOneAndUpdate(filter, update, options);
65
- }
66
- async findOneAndDelete(filter, options) {
67
- return this.collection.findOneAndDelete(filter, options);
68
- }
69
- async findOneAndReplace(filter, replacement, options) {
70
- return this.collection.findOneAndReplace(filter, replacement, options);
71
- }
72
- async countDocuments(filter, options) {
73
- return this.collection.countDocuments(filter, options);
74
- }
75
- async estimatedDocumentCount() {
76
- return this.collection.estimatedDocumentCount();
77
- }
78
- async distinct(field, filter, options) {
79
- return this.collection.distinct(field, filter, options);
80
- }
81
- aggregate(pipeline, options) {
82
- return this.collection.aggregate(pipeline, options);
83
- }
84
- async bulkWrite(operations, options) {
85
- return this.collection.bulkWrite(operations, options);
86
- }
87
- async createIndex(indexSpec, options) {
88
- return this.collection.createIndex(indexSpec, options);
89
- }
90
- async dropIndex(indexName, options) {
91
- return this.collection.dropIndex(indexName, options);
92
- }
93
- };
94
-
95
- // src/entity/builder.ts
96
- var EntityBuilder;
97
- ((EntityBuilder2) => {
98
- function kv(builderInput) {
99
- const adapter = builderInput.adapter();
100
- const result = {
101
- entity(entityInput) {
102
- return new KvEntity(entityInput, adapter);
103
- }
104
- };
105
- return result;
106
- }
107
- EntityBuilder2.kv = kv;
108
- function mongo(builderInput) {
109
- const db = builderInput.adapter();
110
- const result = {
111
- entity(entityInput) {
112
- return new MongoEntity(entityInput, db);
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.`);
13
+ }
14
+ this.blocks.push(block);
15
+ console.debug("[Aromix] Block registered", { block: block.name, phase: "register" });
16
+ }
17
+ onError(handler) {
18
+ this.errorHandlers.push(handler);
19
+ return this;
20
+ }
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);
113
37
  }
114
- };
115
- return result;
116
- }
117
- EntityBuilder2.mongo = mongo;
118
- })(EntityBuilder || (EntityBuilder = {}));
119
-
120
- // src/organize/descriptor.ts
121
- var Descriptor = class {
122
- isExcluded(schema, slot) {
123
- const accessors = schema.state.accessors ?? {};
124
- if (slot === "select") return !!accessors.hidden;
125
- if (slot === "insert") return accessors.readonlyValue !== void 0 || !!accessors.readonlyFn;
126
- return accessors.readonlyValue !== void 0 || !!accessors.readonlyFn || !!accessors.locked;
127
- }
128
- parseSchema(schema, slot) {
129
- const state = schema.state;
130
- if (state.type === "object") {
131
- const shape = state.typeMeta.objectShape ?? {};
132
- const fields = {};
133
- for (const key in shape) {
134
- if (this.isExcluded(shape[key], slot)) continue;
135
- fields[key] = this.parseSchema(shape[key], slot);
136
- }
137
- return { type: "object", shape: fields };
138
- }
139
- if (state.type === "array") {
140
- return { type: "array", element: this.parseSchema(state.typeMeta.arrayElement, slot) };
141
- }
142
- if (state.type === "tuple") {
143
- return { type: "tuple", items: (state.typeMeta.tupleItems ?? []).map((s) => this.parseSchema(s, slot)) };
144
- }
145
- if (state.type === "union") {
146
- return { type: "union", items: (state.typeMeta.unionItems ?? []).map((s) => this.parseSchema(s, slot)) };
147
- }
148
- if (state.type === "record") {
149
- return { type: "record", element: this.parseSchema(state.typeMeta.recordElement, slot) };
150
- }
151
- if (state.type === "literals") {
152
- return { type: "literals", values: state.typeMeta.literalValues };
153
- }
154
- return { type: state.type };
155
- }
156
- kv(entity) {
157
- const name = entity.state.name;
158
- const model = entity.state.model;
159
- return {
160
- name,
161
- platform: "kv",
162
- methods: [
163
- { name: "get", route: `${name}.kv.get`, input: { type: "string" }, output: this.parseSchema(model, "select") },
164
- { name: "set", route: `${name}.kv.set`, input: { type: "object", shape: { key: { type: "string" }, value: this.parseSchema(model, "insert") } }, output: { type: "null" } },
165
- { name: "delete", route: `${name}.kv.delete`, input: { type: "string" }, output: { type: "null" } },
166
- { name: "has", route: `${name}.kv.has`, input: { type: "string" }, output: { type: "boolean" } }
167
- ]
168
- };
169
- }
170
- buildMongoMethod(model, methodName) {
171
- if (methodName === "insertOne") {
172
- return { input: this.parseSchema(model, "insert"), output: { type: "object", shape: { acknowledged: { type: "boolean" }, insertedId: { type: "unknown" } } } };
173
38
  }
174
- if (methodName === "insertMany") {
175
- return {
176
- input: { type: "array", element: this.parseSchema(model, "insert") },
177
- output: { type: "object", shape: { acknowledged: { type: "boolean" }, insertedCount: { type: "number" }, insertedIds: { type: "unknown" } } }
178
- };
179
- }
180
- if (methodName === "findOne") {
181
- return { input: this.parseSchema(model, "select"), output: { type: "union", items: [this.parseSchema(model, "select"), { type: "null" }] } };
182
- }
183
- if (methodName === "find") {
184
- return { input: this.parseSchema(model, "select"), output: { type: "array", element: this.parseSchema(model, "select") } };
185
- }
186
- if (methodName === "updateOne" || methodName === "updateMany") {
187
- return {
188
- input: { type: "object", shape: { filter: this.parseSchema(model, "select"), update: this.parseSchema(model, "update") } },
189
- output: {
190
- type: "object",
191
- shape: {
192
- acknowledged: { type: "boolean" },
193
- matchedCount: { type: "number" },
194
- modifiedCount: { type: "number" },
195
- upsertedCount: { type: "number" },
196
- upsertedId: { type: "unknown" }
197
- }
198
- }
199
- };
200
- }
201
- if (methodName === "replaceOne" || methodName === "findOneAndReplace") {
202
- const updateResultShape = {
203
- type: "object",
204
- shape: { acknowledged: { type: "boolean" }, matchedCount: { type: "number" }, modifiedCount: { type: "number" }, upsertedCount: { type: "number" }, upsertedId: { type: "unknown" } }
205
- };
206
- return {
207
- input: { type: "object", shape: { filter: this.parseSchema(model, "select"), replacement: this.parseSchema(model, "select") } },
208
- output: methodName === "replaceOne" ? updateResultShape : { type: "union", items: [this.parseSchema(model, "select"), { type: "null" }] }
209
- };
210
- }
211
- if (methodName === "deleteOne" || methodName === "deleteMany") {
212
- return { input: this.parseSchema(model, "select"), output: { type: "object", shape: { acknowledged: { type: "boolean" }, deletedCount: { type: "number" } } } };
213
- }
214
- if (methodName === "findOneAndUpdate") {
215
- return {
216
- input: { type: "object", shape: { filter: this.parseSchema(model, "select"), update: this.parseSchema(model, "update") } },
217
- output: { type: "union", items: [this.parseSchema(model, "select"), { type: "null" }] }
218
- };
219
- }
220
- if (methodName === "findOneAndDelete") {
221
- return { input: this.parseSchema(model, "select"), output: { type: "union", items: [this.parseSchema(model, "select"), { type: "null" }] } };
222
- }
223
- if (methodName === "countDocuments") {
224
- return { input: this.parseSchema(model, "select"), output: { type: "number" } };
225
- }
226
- if (methodName === "estimatedDocumentCount") {
227
- return { input: { type: "null" }, output: { type: "number" } };
228
- }
229
- if (methodName === "distinct") {
230
- return { input: { type: "object", shape: { field: { type: "string" }, filter: this.parseSchema(model, "select") } }, output: { type: "unknown" } };
231
- }
232
- if (methodName === "createIndex") {
233
- return { input: { type: "unknown" }, output: { type: "string" } };
234
- }
235
- if (methodName === "dropIndex") {
236
- return { input: { type: "string" }, output: { type: "null" } };
237
- }
238
- return { input: { type: "unknown" }, output: { type: "unknown" } };
239
- }
240
- mongo(entity) {
241
- const name = entity.states.name;
242
- const model = entity.states.model;
243
- const proto = Object.getPrototypeOf(entity);
244
- const methods = [];
245
- for (const key of Object.getOwnPropertyNames(proto)) {
246
- if (key === "constructor") continue;
247
- const shape = this.buildMongoMethod(model, key);
248
- methods.push({ name: key, route: `${name}.mongo.${key}`, input: shape.input, output: shape.output });
249
- }
250
- return { name, platform: "mongo", methods };
251
- }
252
- };
253
-
254
- // src/organize/compose.ts
255
- function compose(input) {
256
- const descriptors = [];
257
- const routes = /* @__PURE__ */ new Map();
258
- const des = new Descriptor();
259
- for (const entity of input.entities) {
260
- if (entity instanceof MongoEntity) {
261
- const descriptor = des.mongo(entity);
262
- descriptors.push(descriptor);
263
- for (const method of descriptor.methods) {
264
- routes.set(method.route, { entity, methodName: method.name });
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 });
51
+ }
52
+ async stop() {
53
+ await this.stopAll("manual");
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 });
265
62
  }
266
63
  }
267
- if (entity instanceof KvEntity) {
268
- const descriptor = des.kv(entity);
269
- descriptors.push(descriptor);
270
- for (const method of descriptor.methods) {
271
- routes.set(method.route, { entity, methodName: method.name });
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 });
272
69
  }
273
70
  }
274
71
  }
275
- return {
276
- get descriptor() {
277
- return { entities: descriptors };
278
- },
279
- async dispatch(route, payload) {
280
- const target = routes.get(route);
281
- if (target === void 0) {
282
- throw new Error(`no method registered for route "${route}"`);
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();
80
+ try {
81
+ await block.stop?.();
82
+ console.log("[Aromix] Block stopped", { block: block.name, phase: "stop", durationMs: Date.now() - startedAt });
83
+ } catch (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);
283
87
  }
284
- const fn = target.entity[target.methodName];
285
- return fn.call(target.entity, payload);
286
- }
287
- };
288
- }
289
-
290
- // src/organize/make.ts
291
- import { createServer } from "http";
292
- async function toWebRequest(req) {
293
- const url = `http://${req.headers.host}${req.url}`;
294
- const headers = new Headers();
295
- for (const key in req.headers) {
296
- const value = req.headers[key];
297
- if (typeof value === "string") {
298
- headers.set(key, value);
299
88
  }
300
- if (Array.isArray(value)) {
301
- headers.set(key, value.join(", "));
302
- }
303
- }
304
- if (req.method === "GET" || req.method === "HEAD") {
305
- return new Request(url, { method: req.method, headers });
306
- }
307
- const chunks = [];
308
- for await (const chunk of req) {
309
- chunks.push(chunk);
89
+ return allStoppedCleanly;
310
90
  }
311
- const body = Buffer.concat(chunks);
312
- return new Request(url, { method: req.method, headers, body });
313
- }
314
- async function writeWebResponse(webResponse, res) {
315
- res.statusCode = webResponse.status;
316
- webResponse.headers.forEach((value, key) => {
317
- res.setHeader(key, value);
318
- });
319
- if (webResponse.body === null) {
320
- res.end();
321
- return;
322
- }
323
- const reader = webResponse.body.getReader();
324
- while (true) {
325
- const next = await reader.read();
326
- if (next.done) {
327
- break;
328
- }
329
- res.write(next.value);
330
- }
331
- res.end();
332
- }
333
- async function handle(request, composed) {
334
- const url = new URL(request.url);
335
- if (url.pathname === "/meta") {
336
- return Response.json(composed.descriptor);
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);
337
95
  }
338
- const route = request.headers.get("x-amx-id");
339
- if (route === null) {
340
- return Response.json({ data: null, errors: null });
341
- }
342
- let payload;
343
- try {
344
- payload = await request.json();
345
- } catch {
346
- payload = void 0;
347
- }
348
- try {
349
- const data = await composed.dispatch(route, payload);
350
- return Response.json({ data, errors: null });
351
- } catch (error) {
352
- let message = "unknown error";
353
- if (error instanceof Error) {
354
- message = error.message;
355
- }
356
- return Response.json({ data: null, errors: message }, { status: 400 });
357
- }
358
- }
359
- function make(composed) {
96
+ };
97
+
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");
360
103
  return {
361
- listen(port) {
362
- const server = createServer((req, res) => {
363
- toWebRequest(req).then((request) => handle(request, composed)).then((response) => writeWebResponse(response, res)).catch((error) => {
364
- res.statusCode = 500;
365
- res.end("internal error");
366
- });
367
- });
368
- server.listen(port);
369
- return server;
104
+ name: "Load Env",
105
+ async start() {
106
+ if (!existsSync(path)) {
107
+ throw new Error(`[Load Env] Environment file not found: ${path}`);
108
+ }
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
+ }
370
118
  }
371
119
  };
372
120
  }
373
121
  export {
374
- Descriptor,
375
- EntityBuilder,
376
- KvEntity,
377
- MongoEntity,
378
- compose,
379
- make
122
+ App,
123
+ loadEnv
380
124
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aromix/core",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "The Core Package For Aromix",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,9 +18,8 @@
18
18
  "core",
19
19
  "server",
20
20
  "framework",
21
- "runtime-agnostic",
22
- "deno",
23
- "node"
21
+ "node",
22
+ "rpc"
24
23
  ],
25
24
  "files": [
26
25
  "dist"
@@ -35,25 +34,39 @@
35
34
  "access": "public",
36
35
  "registry": "https://registry.npmjs.org"
37
36
  },
38
- "scripts": {
39
- "build": "tsup",
40
- "dev": "tsup --watch",
41
- "typecheck": "tsc --noEmit"
42
- },
43
37
  "peerDependencies": {
44
- "@aromix/validator": "^0.3.0",
45
- "mongodb": "^7.3.0"
38
+ "@aromix/validator": "0.5.5",
39
+ "mongodb": "^7.3.0",
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",
48
+ "redis": "^6.0.1",
49
+ "rimraf": "^6.1.3",
51
50
  "tsup": "^8.3.5",
52
- "typescript": "^5.7.2"
51
+ "typescript": "^5.7.2",
52
+ "vitest": "^4.1.6"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "mongodb": {
56
56
  "optional": true
57
+ },
58
+ "redis": {
59
+ "optional": true
57
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"
58
71
  }
59
- }
72
+ }