create-nexwire 4.2.4 → 4.3.0

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 (113) hide show
  1. package/README.md +20 -8
  2. package/express/.env +1 -1
  3. package/express/.vscode/extensions.json +6 -0
  4. package/express/.vscode/settings.json +37 -26
  5. package/express/biome.json +99 -66
  6. package/express/package.json +31 -29
  7. package/express/src/config/database.ts +48 -29
  8. package/express/src/config/index.ts +83 -83
  9. package/express/src/config/openapi.ts +6 -24
  10. package/express/src/config/queue.ts +30 -30
  11. package/express/src/config/validate.ts +50 -40
  12. package/express/src/database/migrations/sqlite/0000_init.sql +47 -47
  13. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  14. package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
  15. package/express/src/database/schema.ts +2 -2
  16. package/express/src/framework/cache/cache.ts +40 -8
  17. package/express/src/framework/circuit-breaker/cb.ts +88 -60
  18. package/express/src/framework/database/connection.ts +52 -27
  19. package/express/src/framework/events/dispatcher.ts +61 -49
  20. package/express/src/framework/facade.ts +46 -3
  21. package/express/src/framework/http/app.ts +28 -22
  22. package/express/src/framework/http/logger.ts +27 -16
  23. package/express/src/framework/http/openapi.ts +9 -11
  24. package/express/src/framework/modules/routes.ts +26 -21
  25. package/express/src/framework/notification/channels/broadcast.ts +15 -0
  26. package/express/src/framework/notification/channels/mail.ts +40 -0
  27. package/express/src/framework/notification/contracts.ts +29 -0
  28. package/express/src/framework/notification/index.ts +19 -94
  29. package/express/src/framework/notification/registry.ts +15 -0
  30. package/express/src/framework/providers/adapter.ts +27 -17
  31. package/express/src/framework/queue/queue.ts +43 -44
  32. package/express/src/framework/realtime/broadcast.ts +29 -5
  33. package/express/src/framework/realtime/socket.ts +149 -119
  34. package/express/src/framework/redis/client.ts +148 -151
  35. package/express/src/framework/runtime/server-adapter.ts +39 -34
  36. package/express/src/framework/scheduler/lock.ts +128 -116
  37. package/express/src/framework/server.ts +33 -49
  38. package/express/src/framework/session/session.ts +69 -14
  39. package/express/src/framework/storage/storage.ts +744 -785
  40. package/express/src/framework/support/mail.ts +15 -7
  41. package/express/src/providers/notification.ts +4 -0
  42. package/express/src/resources/src/components/Checkbox.vue +32 -29
  43. package/express/src/resources/src/components/Input.vue +243 -223
  44. package/express/src/resources/src/components/InputGroup.vue +147 -145
  45. package/express/src/resources/src/components/InputPasswordToggle.vue +229 -213
  46. package/express/src/resources/src/components/Modal.vue +71 -55
  47. package/express/src/resources/src/components/Refresh.vue +19 -0
  48. package/express/src/resources/src/helpers/assets.ts +51 -0
  49. package/express/src/resources/src/layouts/Layout/Header.vue +124 -105
  50. package/express/src/resources/src/main.ts +3 -1
  51. package/express/src/resources/src/plugins/gum.ts +1 -1
  52. package/express/src/resources/src/plugins/pulse.ts +157 -147
  53. package/express/src/resources/src/stores/auth.ts +184 -164
  54. package/express/src/resources/vite.config.ts +54 -36
  55. package/express/src/storage/database/nexwire.sqlite +0 -0
  56. package/express/src/storage/logs/app.log +0 -0
  57. package/express/src/storage/logs/fatal.log +0 -0
  58. package/express/src/types/global.d.ts +32 -26
  59. package/hono/.env +1 -1
  60. package/hono/.vscode/extensions.json +6 -0
  61. package/hono/.vscode/settings.json +37 -26
  62. package/hono/biome.json +25 -2
  63. package/hono/package.json +25 -23
  64. package/hono/src/config/database.ts +48 -29
  65. package/hono/src/config/index.ts +83 -83
  66. package/hono/src/config/openapi.ts +6 -24
  67. package/hono/src/config/queue.ts +30 -30
  68. package/hono/src/config/validate.ts +50 -40
  69. package/hono/src/database/migrations/sqlite/0000_init.sql +47 -47
  70. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  71. package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
  72. package/hono/src/database/schema.ts +2 -2
  73. package/hono/src/framework/cache/cache.ts +40 -8
  74. package/hono/src/framework/circuit-breaker/cb.ts +88 -60
  75. package/hono/src/framework/database/connection.ts +52 -27
  76. package/hono/src/framework/events/dispatcher.ts +61 -49
  77. package/hono/src/framework/facade.ts +38 -1
  78. package/hono/src/framework/http/openapi.ts +7 -9
  79. package/hono/src/framework/notification/channels/broadcast.ts +15 -0
  80. package/hono/src/framework/notification/channels/mail.ts +40 -0
  81. package/hono/src/framework/notification/contracts.ts +29 -0
  82. package/hono/src/framework/notification/index.ts +19 -94
  83. package/hono/src/framework/notification/registry.ts +15 -0
  84. package/hono/src/framework/providers/adapter.ts +27 -17
  85. package/hono/src/framework/queue/queue.ts +43 -44
  86. package/hono/src/framework/realtime/broadcast.ts +29 -5
  87. package/hono/src/framework/realtime/socket.ts +217 -196
  88. package/hono/src/framework/redis/client.ts +148 -151
  89. package/hono/src/framework/scheduler/lock.ts +128 -116
  90. package/hono/src/framework/server.ts +27 -29
  91. package/hono/src/framework/session/session.ts +62 -14
  92. package/hono/src/framework/storage/storage.ts +744 -785
  93. package/hono/src/framework/support/mail.ts +56 -48
  94. package/hono/src/providers/notification.ts +4 -0
  95. package/hono/src/resources/src/components/Checkbox.vue +32 -29
  96. package/hono/src/resources/src/components/Input.vue +243 -234
  97. package/hono/src/resources/src/components/InputGroup.vue +147 -149
  98. package/hono/src/resources/src/components/InputPasswordToggle.vue +229 -222
  99. package/hono/src/resources/src/components/Modal.vue +71 -55
  100. package/hono/src/resources/src/components/Refresh.vue +19 -0
  101. package/hono/src/resources/src/helpers/assets.ts +51 -0
  102. package/hono/src/resources/src/layouts/Layout/Header.vue +124 -105
  103. package/hono/src/resources/src/main.ts +3 -1
  104. package/hono/src/resources/src/plugins/gum.ts +1 -1
  105. package/hono/src/resources/src/plugins/pulse.ts +157 -147
  106. package/hono/src/resources/src/stores/auth.ts +184 -164
  107. package/hono/src/resources/vite.config.ts +54 -36
  108. package/hono/src/storage/database/nexwire.sqlite +0 -0
  109. package/hono/src/storage/logs/app.log +0 -0
  110. package/hono/src/storage/logs/fatal.log +0 -0
  111. package/hono/src/types/global.d.ts +32 -26
  112. package/package.json +2 -2
  113. package/src/index.js +139 -14
@@ -1,2 +1,2 @@
1
- export * from "../modules/auth/database/models/role.js";
2
- export * from "../modules/auth/database/models/user.js";
1
+ export * from "../modules/auth/database/models/role.js";
2
+ export * from "../modules/auth/database/models/user.js";
@@ -1,10 +1,18 @@
1
1
  import { cacheConfig, redisConfig } from "@/config/index.js";
2
+ import { createCircuitBreaker, isCircuitBreakerError } from "@/framework/circuit-breaker/cb.js";
2
3
  import { redisClientIfReady } from "@/framework/redis/client.js";
3
4
 
4
5
  function cacheKey(key: string) {
5
6
  return `${cacheConfig.keyPrefix}:${key}`;
6
7
  }
7
8
 
9
+ // Initialize the circuit breaker specifically for Cache operations
10
+ const cacheBreaker = createCircuitBreaker({
11
+ name: "Redis Cache",
12
+ failureThreshold: 3,
13
+ resetTimeout: 30000
14
+ });
15
+
8
16
  /**
9
17
  * Why: Provides Redis-backed cache helpers with graceful fallback behavior.
10
18
  * When: Controllers/jobs need temporary computed data caching.
@@ -20,18 +28,35 @@ export const cache = {
20
28
  */
21
29
  async get<T>(key: string, fallback: T | null = null) {
22
30
  const client = redisClientIfReady();
31
+ // If the client isn't connected at all, fail fast to fallback
23
32
  if (!client) return fallback;
24
33
 
25
- const value = await client.get(cacheKey(key));
26
- return value ? (JSON.parse(value) as T) : fallback;
34
+ try {
35
+ return await cacheBreaker.exec(async () => {
36
+ const value = await client.get(cacheKey(key));
37
+ return value ? (JSON.parse(value) as T) : fallback;
38
+ });
39
+ } catch (error) {
40
+ // If the circuit is open, or a Redis error occurs, return the fallback safely
41
+ if (isCircuitBreakerError(error)) return fallback;
42
+ return fallback;
43
+ }
27
44
  },
28
45
 
29
46
  async set(key: string, value: unknown, ttl = cacheConfig.ttlSeconds) {
30
47
  const client = redisClientIfReady();
31
48
  if (!client) return false;
32
49
 
33
- await client.set(cacheKey(key), JSON.stringify(value), "EX", ttl);
34
- return true;
50
+ try {
51
+ return await cacheBreaker.exec(async () => {
52
+ await client.set(cacheKey(key), JSON.stringify(value), "EX", ttl);
53
+ return true;
54
+ });
55
+ } catch (error) {
56
+ // If the circuit is open, or a Redis error occurs, silently fail
57
+ if (isCircuitBreakerError(error)) return false;
58
+ return false;
59
+ }
35
60
  },
36
61
 
37
62
  /**
@@ -51,8 +76,15 @@ export const cache = {
51
76
  const client = redisClientIfReady();
52
77
  if (!client) return false;
53
78
 
54
- await client.del(cacheKey(key));
55
- return true;
79
+ try {
80
+ return await cacheBreaker.exec(async () => {
81
+ await client.del(cacheKey(key));
82
+ return true;
83
+ });
84
+ } catch (error) {
85
+ if (isCircuitBreakerError(error)) return false;
86
+ return false;
87
+ }
56
88
  },
57
89
 
58
90
  /**
@@ -66,7 +98,7 @@ export const cache = {
66
98
  if (cached !== null) return cached;
67
99
 
68
100
  const fresh = await callback();
69
- await this.put(key, fresh, ttl);
101
+ await this.set(key, fresh, ttl);
70
102
  return fresh;
71
103
  },
72
104
 
@@ -77,6 +109,6 @@ export const cache = {
77
109
  * How: Confirms REDIS flag and ready Redis client instance.
78
110
  */
79
111
  isAvailable() {
80
- return redisConfig.enabled && redisClientIfReady() !== null;
112
+ return redisConfig.enabled && redisClientIfReady() !== null && cacheBreaker.getState() !== "open";
81
113
  }
82
114
  };
@@ -1,60 +1,88 @@
1
- export class CircuitBreaker {
2
- private state: "closed" | "open" | "half-open" = "closed";
3
- private failureCount = 0;
4
- private readonly failureThreshold = 3;
5
- private readonly resetTimeout = 30000;
6
- private lastFailureTime = 0;
7
- private successCount = 0;
8
- private readonly successThreshold = 1;
9
-
10
- async exec<T>(fn: () => Promise<T>): Promise<T> {
11
- if (this.state === "open") {
12
- if (Date.now() - this.lastFailureTime >= this.resetTimeout) {
13
- this.state = "half-open";
14
- this.successCount = 0;
15
- } else {
16
- throw new Error("Circuit breaker is open");
17
- }
18
- }
19
- try {
20
- const result = await fn();
21
- this.onSuccess();
22
- return result;
23
- } catch (error) {
24
- this.onFailure();
25
- throw error;
26
- }
27
- }
28
-
29
- getState(): "closed" | "open" | "half-open" {
30
- return this.state;
31
- }
32
-
33
- onSuccess(): void {
34
- if (this.state === "half-open") {
35
- this.successCount++;
36
- if (this.successCount >= this.successThreshold) {
37
- this.reset();
38
- }
39
- }
40
- this.failureCount = 0;
41
- }
42
-
43
- onFailure(): void {
44
- this.failureCount++;
45
- if (this.failureCount >= this.failureThreshold || this.state === "half-open") {
46
- this.trip();
47
- }
48
- }
49
-
50
- trip(): void {
51
- this.state = "open";
52
- this.lastFailureTime = Date.now();
53
- }
54
-
55
- reset(): void {
56
- this.state = "closed";
57
- this.failureCount = 0;
58
- this.successCount = 0;
59
- }
60
- }
1
+ // Factory for the error
2
+ export function createCircuitBreakerError(serviceName: string) {
3
+ const error = new Error(`Circuit breaker is open for ${serviceName}. Service is temporarily unavailable.`);
4
+ // Attach a custom property to identify it
5
+ Object.assign(error, { name: "CircuitBreakerOpenError", isCircuitBreakerError: true });
6
+ return error;
7
+ }
8
+
9
+ // Functional Type Guard
10
+ export function isCircuitBreakerError(error: unknown): error is Error {
11
+ return error instanceof Error && (error as any).isCircuitBreakerError === true;
12
+ }
13
+
14
+ export interface CircuitBreakerOptions {
15
+ name?: string;
16
+ failureThreshold?: number;
17
+ resetTimeout?: number;
18
+ successThreshold?: number;
19
+ }
20
+
21
+ export function createCircuitBreaker({
22
+ name = "Unknown Service",
23
+ failureThreshold = 3,
24
+ resetTimeout = 30000,
25
+ successThreshold = 1
26
+ }: CircuitBreakerOptions = {}) {
27
+ let state: "closed" | "open" | "half-open" = "closed";
28
+ let failureCount = 0;
29
+ let successCount = 0;
30
+ let lastFailureTime = 0;
31
+ let isHalfOpenTestRunning = false;
32
+
33
+ function trip() {
34
+ state = "open";
35
+ lastFailureTime = Date.now();
36
+ }
37
+
38
+ function reset() {
39
+ state = "closed";
40
+ failureCount = 0;
41
+ successCount = 0;
42
+ }
43
+
44
+ return {
45
+ async exec<T>(fn: () => Promise<T>): Promise<T> {
46
+ if (state === "open") {
47
+ if (Date.now() - lastFailureTime >= resetTimeout) {
48
+ state = "half-open";
49
+ successCount = 0;
50
+ isHalfOpenTestRunning = false;
51
+ } else {
52
+ throw createCircuitBreakerError(name);
53
+ }
54
+ }
55
+
56
+ if (state === "half-open") {
57
+ if (isHalfOpenTestRunning) {
58
+ throw createCircuitBreakerError(name);
59
+ }
60
+ isHalfOpenTestRunning = true;
61
+ }
62
+
63
+ try {
64
+ const result = await fn();
65
+ if (state === "half-open") {
66
+ successCount++;
67
+ if (successCount >= successThreshold) reset();
68
+ }
69
+ failureCount = 0;
70
+ return result;
71
+ } catch (error) {
72
+ failureCount++;
73
+ if (failureCount >= failureThreshold || state === "half-open") {
74
+ trip();
75
+ }
76
+ throw error;
77
+ } finally {
78
+ if (state === "half-open") isHalfOpenTestRunning = false;
79
+ }
80
+ },
81
+
82
+ getState() {
83
+ return state;
84
+ }
85
+ };
86
+ }
87
+
88
+ export type CircuitBreaker = ReturnType<typeof createCircuitBreaker>;
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { databaseConfig } from "@/config/index.js";
3
+ import { databaseConfig, detectDialectFrom } from "@/config/index.js";
4
4
  import { loadAppSchema } from "@/framework/database/app-schema.js";
5
5
 
6
6
  export type Dialect = "sqlite" | "mysql" | "postgresql";
@@ -20,10 +20,7 @@ async function optionalImport<T = any>(name: string): Promise<T> {
20
20
  * How: Checks DATABASE_URL prefix and maps to sqlite/mysql/postgresql.
21
21
  */
22
22
  export function detectDialect(): Dialect {
23
- const url = databaseConfig.url.toLowerCase();
24
- if (url.startsWith("mysql")) return "mysql";
25
- if (url.startsWith("postgres")) return "postgresql";
26
- return "sqlite";
23
+ return detectDialectFrom(databaseConfig.url);
27
24
  }
28
25
 
29
26
  /**
@@ -36,9 +33,9 @@ export async function initDatabase() {
36
33
  if (databaseInstance) return databaseInstance;
37
34
 
38
35
  dialect = detectDialect();
39
-
40
36
  const schema = await loadAppSchema();
41
37
 
38
+ // --- SQLite (libsql) ---
42
39
  if (dialect === "sqlite") {
43
40
  let drizzleSqlite: any;
44
41
  let createClient: any;
@@ -51,13 +48,24 @@ export async function initDatabase() {
51
48
  throw new Error("Missing sqlite dependencies. Install with: bun add drizzle-orm @libsql/client");
52
49
  }
53
50
 
54
- const file = path.resolve(process.cwd(), databaseConfig.url.replace(/^sqlite:/, ""));
55
- await fs.mkdir(path.dirname(file), { recursive: true });
56
- pool = createClient({ url: `file:${file.replace(/\\/g, "/")}` });
51
+ // databaseConfig.url is guaranteed to be "file:..."
52
+ const url = databaseConfig.url;
53
+
54
+ // Ensure the directory exists (skip for :memory: or absolute URLs)
55
+ if (!url.includes(":memory:")) {
56
+ const rawPath = url.replace(/^file:/i, "");
57
+ if (!rawPath.startsWith("//")) {
58
+ const file = path.resolve(process.cwd(), rawPath);
59
+ await fs.mkdir(path.dirname(file), { recursive: true });
60
+ }
61
+ }
62
+
63
+ pool = createClient({ url });
57
64
  databaseInstance = drizzleSqlite(pool, { schema });
58
65
  return databaseInstance;
59
66
  }
60
67
 
68
+ // --- MySQL ---
61
69
  if (dialect === "mysql") {
62
70
  let drizzleMysql: any;
63
71
  let mysql: any;
@@ -67,11 +75,19 @@ export async function initDatabase() {
67
75
  throw new Error("Missing mysql dependencies. Install with: bun add drizzle-orm mysql2");
68
76
  }
69
77
 
70
- pool = mysql.createPool({ uri: databaseConfig.url, connectionLimit: 10, enableKeepAlive: true });
78
+ pool = mysql.createPool({
79
+ uri: databaseConfig.url,
80
+ connectionLimit: databaseConfig.pool.max,
81
+ connectTimeout: databaseConfig.pool.connectTimeout,
82
+ enableKeepAlive: databaseConfig.pool.keepAlive,
83
+ ...(databaseConfig.ssl ? { ssl: {} } : {})
84
+ });
85
+
71
86
  databaseInstance = drizzleMysql(pool, { schema, mode: "default" });
72
87
  return databaseInstance;
73
88
  }
74
89
 
90
+ // --- PostgreSQL ---
75
91
  let drizzlePg: any;
76
92
  let postgres: any;
77
93
  try {
@@ -83,7 +99,14 @@ export async function initDatabase() {
83
99
  throw new Error("Missing postgres dependencies. Install with: bun add drizzle-orm postgres");
84
100
  }
85
101
 
86
- pool = postgres(databaseConfig.url);
102
+ pool = postgres(databaseConfig.url, {
103
+ max: databaseConfig.pool.max,
104
+ idle_timeout: Math.floor(databaseConfig.pool.idleTimeout / 1000),
105
+ connect_timeout: Math.floor(databaseConfig.pool.connectTimeout / 1000),
106
+ prepare: databaseConfig.prepare,
107
+ ...(databaseConfig.ssl ? { ssl: "require" as const } : {})
108
+ });
109
+
87
110
  databaseInstance = drizzlePg(pool, { schema });
88
111
  return databaseInstance;
89
112
  }
@@ -121,23 +144,20 @@ export async function closeDatabase() {
121
144
 
122
145
  const activeDialect = databaseDialect();
123
146
 
124
- if (activeDialect === "sqlite") {
125
- pool.close?.();
126
- pool = undefined;
127
- databaseInstance = undefined;
128
- return;
129
- }
130
-
131
- if (activeDialect === "mysql") {
132
- await pool.end();
147
+ try {
148
+ if (activeDialect === "sqlite") {
149
+ await pool.close?.();
150
+ } else if (activeDialect === "mysql") {
151
+ await pool.end();
152
+ } else {
153
+ await pool.end();
154
+ }
155
+ } catch (error) {
156
+ console.error("Error closing database pool:", error);
157
+ } finally {
133
158
  pool = undefined;
134
159
  databaseInstance = undefined;
135
- return;
136
160
  }
137
-
138
- await pool.end();
139
- pool = undefined;
140
- databaseInstance = undefined;
141
161
  }
142
162
 
143
163
  /**
@@ -146,7 +166,7 @@ export async function closeDatabase() {
146
166
  * Where: Locking, schema, and shutdown helpers.
147
167
  * How: Uses initialized dialect or falls back to detection.
148
168
  */
149
- export function databaseDialect() {
169
+ export function databaseDialect(): Dialect {
150
170
  return dialect || detectDialect();
151
171
  }
152
172
 
@@ -185,12 +205,17 @@ export const db = new Proxy(
185
205
  {
186
206
  get(_target, property) {
187
207
  const instance = database();
208
+
188
209
  if (property === "execute") {
189
210
  return async (query: unknown, params?: unknown) => {
190
- const raw = instance.execute ? await instance.execute(query, params) : await instance.all(query);
211
+ if (!instance.execute) {
212
+ throw new Error("The current driver does not support raw execute.");
213
+ }
214
+ const raw = await instance.execute(query, params);
191
215
  return normalizeExecuteResult(raw);
192
216
  };
193
217
  }
218
+
194
219
  const value = instance[property as keyof typeof instance];
195
220
  return typeof value === "function" ? value.bind(instance) : value;
196
221
  }
@@ -1,49 +1,61 @@
1
- import { queueJob } from "@/framework/queue/queue.js";
2
- import { type BroadcastOptions, broadcast } from "@/framework/realtime/broadcast.js";
3
-
4
- type CommandHandler = (payload: any) => Promise<any>;
5
-
6
- const commandHandlers = new Map<string, CommandHandler>();
7
-
8
- /**
9
- * Why: Registers in-memory command handler for synchronous dispatch.
10
- * When: Module startup files register command handlers.
11
- * Where: Job/command style module code.
12
- * How: Stores handler in a process-level map by command name.
13
- */
14
- export function command(name: string, handler: CommandHandler) {
15
- commandHandlers.set(name, handler);
16
- }
17
-
18
- /**
19
- * Why: Executes command immediately or queues it for async processing.
20
- * When: Application logic needs command pattern behavior.
21
- * Where: Controllers, jobs, and service layers.
22
- * How: Runs local handler unless async requested; otherwise enqueues.
23
- */
24
- export async function dispatchCommand(name: string, payload: any, options: { async?: boolean; queue?: string } = {}) {
25
- if (!options.async) {
26
- const handler = commandHandlers.get(name);
27
- if (handler) return await handler(payload);
28
- }
29
-
30
- return await queueJob(name, payload, { queue: options.queue || "default" });
31
- }
32
-
33
- /**
34
- * Why: Dispatches domain events with optional queueing and websocket fan-out.
35
- * When: Domain actions need side effects or realtime notifications.
36
- * Where: Controllers/jobs across modules.
37
- * How: Broadcasts via Socket.IO and optionally enqueues job.
38
- */
39
- export async function dispatchEvent(name: string, payload: any, options: { queue?: boolean | string; broadcast?: BroadcastOptions } = {}) {
40
- if (options.broadcast) {
41
- broadcast(name, payload, options.broadcast);
42
- }
43
-
44
- if (options.queue) {
45
- return await queueJob(name, payload, {
46
- queue: typeof options.queue === "string" ? options.queue : "default"
47
- });
48
- }
49
- }
1
+ import { queueJob } from "@/framework/queue/queue.js";
2
+ import { type BroadcastOptions, broadcast } from "@/framework/realtime/broadcast.js";
3
+ import { logger } from "@/framework/support/logger.js";
4
+
5
+ type CommandHandler = (payload: any) => Promise<any>;
6
+
7
+ const commandHandlers = new Map<string, CommandHandler>();
8
+
9
+ /**
10
+ * Why: Registers in-memory command handler for synchronous dispatch.
11
+ * When: Module startup files register command handlers.
12
+ * Where: Job/command style module code.
13
+ * How: Stores handler in a process-level map by command name.
14
+ */
15
+ export function command(name: string, handler: CommandHandler) {
16
+ commandHandlers.set(name, handler);
17
+ }
18
+
19
+ /**
20
+ * Why: Executes command immediately or queues it for async processing.
21
+ * When: Application logic needs command pattern behavior.
22
+ * Where: Controllers, jobs, and service layers.
23
+ * How: Runs local handler unless async requested; otherwise enqueues.
24
+ */
25
+ export async function dispatchCommand(name: string, payload: any, options: { async?: boolean; queue?: string } = {}) {
26
+ if (!options.async) {
27
+ const handler = commandHandlers.get(name);
28
+ if (handler) return await handler(payload);
29
+ }
30
+
31
+ return await queueJob(name, payload, { queue: options.queue || "default" });
32
+ }
33
+
34
+ /**
35
+ * Why: Dispatches domain events with optional queueing and websocket fan-out.
36
+ * When: Domain actions need side effects or realtime notifications.
37
+ * Where: Controllers/jobs across modules.
38
+ * How: Broadcasts via Socket.IO and optionally enqueues job.
39
+ */
40
+ export async function dispatchEvent(name: string, payload: any, options: { queue?: boolean | string; broadcast?: BroadcastOptions } = {}) {
41
+ // 1. Handle Broadcast safely
42
+ if (options.broadcast) {
43
+ try {
44
+ // Await the broadcast so the circuit breaker and redis publish complete safely
45
+ await broadcast(name, payload, options.broadcast);
46
+ } catch (error) {
47
+ logger.error(`Failed to broadcast event: ${name}`, { error });
48
+ }
49
+ }
50
+
51
+ // 2. Handle Queue safely
52
+ if (options.queue) {
53
+ try {
54
+ return await queueJob(name, payload, {
55
+ queue: typeof options.queue === "string" ? options.queue : "default"
56
+ });
57
+ } catch (error) {
58
+ logger.error(`Failed to queue event: ${name}`, { error });
59
+ }
60
+ }
61
+ }
@@ -5,17 +5,43 @@
5
5
  * How: Re-exports stable, reusable helpers while hiding setup internals.
6
6
  */
7
7
 
8
+ // ==========================================
9
+ // 1. External & Core Utilities
10
+ // ==========================================
8
11
  export { createRoute, z } from "@hono/zod-openapi";
9
12
  export * as lodash from "lodash-es";
10
13
  export * as HttpStatusCodes from "stoker/http-status-codes";
11
14
  export { jsonContent } from "stoker/openapi/helpers";
15
+ // ==========================================
16
+ // 6. Cache & Session
17
+ // ==========================================
12
18
  export { cache } from "@/framework/cache/cache.js";
19
+ // ==========================================
20
+ // 2. Database
21
+ // ==========================================
13
22
  export { database, db } from "@/framework/database/connection.js";
14
23
  export { type PaginatedResult, paginate, paginateModel, paginateQuery, paginateTable } from "@/framework/database/paginate.js";
24
+ // ==========================================
25
+ // 4. Events & Queue
26
+ // ==========================================
15
27
  export { command, dispatchCommand, dispatchEvent } from "@/framework/events/dispatcher.js";
28
+ // ==========================================
29
+ // 3. HTTP & Routing
30
+ // ==========================================
16
31
  export { createRouter, group } from "@/framework/http/router.js";
17
32
  export { validate } from "@/framework/http/validation.js";
33
+ export { broadcastChannel } from "@/framework/notification/channels/broadcast.js";
34
+ export { mailChannel } from "@/framework/notification/channels/mail.js";
35
+ export type { NotificationChannel, NotificationOptions } from "@/framework/notification/contracts.js";
36
+ // ==========================================
37
+ // 10. Notification Engine (Delivery Channels)
38
+ // ==========================================
18
39
  export { notify } from "@/framework/notification/index.js";
40
+ export { registerChannel } from "@/framework/notification/registry.js";
41
+ // ==========================================
42
+ // 9. Providers (Storage/Auth)
43
+ // ==========================================
44
+ export { resolveAuthProvider, resolveNotificationProvider } from "@/framework/providers/adapter.js";
19
45
  export type {
20
46
  AuthProvider,
21
47
  AuthProviderRefreshToken,
@@ -29,7 +55,6 @@ export type {
29
55
  NotificationProvider,
30
56
  NotificationRecord
31
57
  } from "@/framework/providers/contracts.js";
32
- export { resolveAuthProvider, resolveNotificationProvider } from "@/framework/providers/adapter.js";
33
58
  export { loadProviders } from "@/framework/providers/loader.js";
34
59
  export {
35
60
  clearAuthProvider,
@@ -47,12 +72,24 @@ export {
47
72
  registerProvider
48
73
  } from "@/framework/providers/registry.js";
49
74
  export { queue, queueJob, shouldQueue } from "@/framework/queue/queue.js";
75
+ // ==========================================
76
+ // 5. Realtime
77
+ // ==========================================
50
78
  export { broadcast } from "@/framework/realtime/index.js";
51
79
  export { defineSchedule } from "@/framework/scheduler/scheduler.js";
52
80
  export { session } from "@/framework/session/session.js";
81
+ // ==========================================
82
+ // 7. Storage & Mail
83
+ // ==========================================
53
84
  export { storage } from "@/framework/storage/storage.js";
85
+ // ==========================================
86
+ // 8. Security & Auth
87
+ // ==========================================
54
88
  export { cookie } from "@/framework/support/cookie.js";
55
89
  export { jwt } from "@/framework/support/jwt.js";
90
+ // ==========================================
91
+ // 11. Utilities
92
+ // ==========================================
56
93
  export { logger } from "@/framework/support/logger.js";
57
94
  export { mail } from "@/framework/support/mail.js";
58
95
  export { password } from "@/framework/support/password.js";
@@ -11,6 +11,8 @@ import type { NexwireRouter } from "@/framework/http/router.js";
11
11
  * How: Exposes `/doc` spec and `/api-docs` Scalar viewer.
12
12
  */
13
13
  export function configureOpenApi(app: NexwireRouter) {
14
+ const { specUrl, docsPath, favicon, ...scalarOptions } = openApiConfig.scalar;
15
+
14
16
  app.doc("/doc", {
15
17
  openapi: openApiConfig.version,
16
18
  info: {
@@ -20,23 +22,19 @@ export function configureOpenApi(app: NexwireRouter) {
20
22
  }
21
23
  });
22
24
 
23
- if (openApiConfig.scalar.favicon) {
25
+ if (favicon) {
24
26
  app.get("/favicon.ico", (c) => {
25
- const faviconPath = path.join(process.cwd(), openApiConfig.scalar.favicon);
27
+ const faviconPath = path.join(process.cwd(), favicon);
26
28
  if (!fs.existsSync(faviconPath)) return c.notFound();
27
29
  return c.body(fs.readFileSync(faviconPath), 200, { "content-type": "image/x-icon" });
28
30
  });
29
31
  }
30
32
 
31
33
  app.get(
32
- openApiConfig.scalar.docsPath,
34
+ docsPath, // Use the destructured path
33
35
  Scalar({
34
- url: openApiConfig.scalar.specUrl,
35
- layout: openApiConfig.scalar.layout,
36
- theme: openApiConfig.scalar.theme,
37
- pageTitle: openApiConfig.scalar.pageTitle,
38
- defaultHttpClient: openApiConfig.scalar.defaultHttpClient,
39
- defaultOpenAllTags: openApiConfig.scalar.defaultOpenAllTags
36
+ url: specUrl, // Use the destructured URL
37
+ ...scalarOptions // Pass EVERYTHING else automatically!
40
38
  })
41
39
  );
42
40
  }
@@ -0,0 +1,15 @@
1
+ import { dispatchEvent } from "@/framework/events/dispatcher.js";
2
+ import type { NotificationChannel } from "@/framework/notification/contracts.js";
3
+
4
+ export const broadcastChannel: NotificationChannel = {
5
+ name: "broadcast",
6
+ async send(target, notification, options) {
7
+ if (!options.broadcast) return false;
8
+
9
+ await dispatchEvent("notification.created", notification, {
10
+ broadcast: { users: [target.id] }
11
+ });
12
+
13
+ return true;
14
+ }
15
+ };