create-nexwire 4.2.5 → 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 (85) hide show
  1. package/README.md +20 -8
  2. package/express/.env +1 -1
  3. package/express/package.json +1 -1
  4. package/express/src/config/database.ts +48 -29
  5. package/express/src/config/index.ts +83 -83
  6. package/express/src/config/openapi.ts +6 -24
  7. package/express/src/config/queue.ts +30 -30
  8. package/express/src/config/validate.ts +50 -40
  9. package/express/src/database/migrations/sqlite/0000_init.sql +47 -47
  10. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  11. package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
  12. package/express/src/framework/cache/cache.ts +40 -8
  13. package/express/src/framework/circuit-breaker/cb.ts +88 -60
  14. package/express/src/framework/database/connection.ts +52 -27
  15. package/express/src/framework/events/dispatcher.ts +61 -49
  16. package/express/src/framework/facade.ts +46 -3
  17. package/express/src/framework/http/app.ts +28 -22
  18. package/express/src/framework/http/logger.ts +27 -16
  19. package/express/src/framework/http/openapi.ts +9 -11
  20. package/express/src/framework/modules/routes.ts +26 -21
  21. package/express/src/framework/notification/channels/broadcast.ts +15 -0
  22. package/express/src/framework/notification/channels/mail.ts +40 -0
  23. package/express/src/framework/notification/contracts.ts +29 -0
  24. package/express/src/framework/notification/index.ts +19 -94
  25. package/express/src/framework/notification/registry.ts +15 -0
  26. package/express/src/framework/providers/adapter.ts +27 -17
  27. package/express/src/framework/queue/queue.ts +43 -44
  28. package/express/src/framework/realtime/broadcast.ts +29 -5
  29. package/express/src/framework/realtime/socket.ts +149 -119
  30. package/express/src/framework/redis/client.ts +148 -151
  31. package/express/src/framework/runtime/server-adapter.ts +39 -34
  32. package/express/src/framework/scheduler/lock.ts +128 -116
  33. package/express/src/framework/server.ts +33 -49
  34. package/express/src/framework/session/session.ts +69 -14
  35. package/express/src/framework/storage/storage.ts +744 -785
  36. package/express/src/framework/support/mail.ts +15 -7
  37. package/express/src/providers/notification.ts +4 -0
  38. package/express/src/resources/src/layouts/Layout/Header.vue +124 -105
  39. package/express/src/resources/src/plugins/pulse.ts +157 -147
  40. package/express/src/resources/vite.config.ts +54 -43
  41. package/express/src/storage/database/nexwire.sqlite +0 -0
  42. package/express/src/storage/logs/app.log +0 -0
  43. package/express/src/storage/logs/fatal.log +0 -0
  44. package/express/src/types/global.d.ts +32 -26
  45. package/hono/.env +1 -1
  46. package/hono/package.json +1 -1
  47. package/hono/src/config/database.ts +48 -29
  48. package/hono/src/config/index.ts +83 -83
  49. package/hono/src/config/openapi.ts +6 -24
  50. package/hono/src/config/queue.ts +30 -30
  51. package/hono/src/config/validate.ts +50 -40
  52. package/hono/src/database/migrations/sqlite/0000_init.sql +47 -47
  53. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  54. package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
  55. package/hono/src/framework/cache/cache.ts +40 -8
  56. package/hono/src/framework/circuit-breaker/cb.ts +88 -60
  57. package/hono/src/framework/database/connection.ts +52 -27
  58. package/hono/src/framework/events/dispatcher.ts +61 -49
  59. package/hono/src/framework/facade.ts +38 -1
  60. package/hono/src/framework/http/openapi.ts +7 -9
  61. package/hono/src/framework/notification/channels/broadcast.ts +15 -0
  62. package/hono/src/framework/notification/channels/mail.ts +40 -0
  63. package/hono/src/framework/notification/contracts.ts +29 -0
  64. package/hono/src/framework/notification/index.ts +19 -94
  65. package/hono/src/framework/notification/registry.ts +15 -0
  66. package/hono/src/framework/providers/adapter.ts +27 -17
  67. package/hono/src/framework/queue/queue.ts +43 -44
  68. package/hono/src/framework/realtime/broadcast.ts +29 -5
  69. package/hono/src/framework/realtime/socket.ts +217 -196
  70. package/hono/src/framework/redis/client.ts +148 -151
  71. package/hono/src/framework/scheduler/lock.ts +128 -116
  72. package/hono/src/framework/server.ts +27 -29
  73. package/hono/src/framework/session/session.ts +62 -14
  74. package/hono/src/framework/storage/storage.ts +744 -785
  75. package/hono/src/framework/support/mail.ts +56 -48
  76. package/hono/src/providers/notification.ts +4 -0
  77. package/hono/src/resources/src/layouts/Layout/Header.vue +124 -105
  78. package/hono/src/resources/src/plugins/pulse.ts +157 -147
  79. package/hono/src/resources/vite.config.ts +54 -43
  80. package/hono/src/storage/database/nexwire.sqlite +0 -0
  81. package/hono/src/storage/logs/app.log +0 -0
  82. package/hono/src/storage/logs/fatal.log +0 -0
  83. package/hono/src/types/global.d.ts +32 -26
  84. package/package.json +2 -2
  85. package/src/index.js +139 -14
@@ -5,7 +5,7 @@ import { appConfig, mailConfig, realtimeConfig, redisConfig } from "@/config/ind
5
5
  import { createKernel } from "@/framework/kernel.js";
6
6
  import { stopQueueRuntime } from "@/framework/queue/queue.js";
7
7
  import { stopQueueDashboard } from "@/framework/queue/ui.js";
8
- import { broadcast, closeRealtime, initRealtime, WS_PATH } from "@/framework/realtime/index.js";
8
+ import { broadcast, closeRealtime, socketServer, WS_PATH } from "@/framework/realtime/index.js";
9
9
  import { setupSocketAdminUI } from "@/framework/realtime/ui.js";
10
10
  import { closeRedis, redisClientIfReady, redisError, redisReady } from "@/framework/redis/client.js";
11
11
  import { type HttpServerHandle, startHttpServer } from "@/framework/runtime/server-adapter.js";
@@ -14,23 +14,15 @@ import { logger } from "@/framework/support/logger.js";
14
14
 
15
15
  const redisBackedServices = "cache, session, queue, events, Queue Dashboard";
16
16
 
17
- /**
18
- * Why: Builds reliable local URL string for runtime service output.
19
- * When: Printing API/docs/Queue Dashboard endpoints at startup.
20
- * Where: Server runtime logging.
21
- * How: Derives bound port from server address and normalizes hostname.
22
- */
23
17
  function serverUrl(server: HttpServerHandle, pathname = "") {
24
18
  const address = server.address();
25
19
  const port = typeof address === "object" && address ? address.port : appConfig.port;
26
-
27
20
  try {
28
21
  const currentUrl = new URL(appConfig.url);
29
22
  currentUrl.port = String(port);
30
23
  if (currentUrl.hostname === "0.0.0.0" || currentUrl.hostname === "::") {
31
24
  currentUrl.hostname = "localhost";
32
25
  }
33
-
34
26
  return `${currentUrl.toString().replace(/\/$/, "")}${pathname}`;
35
27
  } catch {
36
28
  return `http://localhost:${port}${pathname}`;
@@ -41,27 +33,22 @@ export interface ServerHandle {
41
33
  app: Awaited<ReturnType<typeof createKernel>>["app"];
42
34
  bullBoard: Awaited<ReturnType<typeof createKernel>>["bullBoard"];
43
35
  server: HttpServerHandle;
44
- realtime: Awaited<ReturnType<typeof initRealtime>>;
36
+ realtime: any; // Type for Express is simpler
45
37
  broadcastSubClient: ReturnType<typeof redisClientIfReady> | null;
46
38
  shutdown: (signal: ShutdownSignal) => Promise<void>;
47
39
  closeHttpServer: () => Promise<void>;
48
40
  }
49
41
 
50
- /**
51
- * Why: Starts the HTTP server and wires realtime/queue/redis shutdown.
52
- * When: CLI `serve` command or E2E test bootstrap.
53
- * Where: Server entrypoint.
54
- * How: Creates kernel, starts HTTP, registers signal handlers, returns handle.
55
- * When called as the main module, also prints startup banners.
56
- */
57
42
  export async function startServer(): Promise<ServerHandle> {
58
43
  const { app, bullBoard } = await createKernel();
59
44
 
60
- let realtime = null as Awaited<ReturnType<typeof initRealtime>>;
45
+ // 1. Start the server (this internally attaches Socket.IO for Express)
61
46
  const server = await startHttpServer(app, appConfig.port);
62
- realtime = await initRealtime({ httpServer: server.native });
63
47
 
64
- const socketAdmin = setupSocketAdminUI(realtime?.io ?? null);
48
+ // 2. Grab the existing Socket.IO instance
49
+ const io = socketServer();
50
+ const realtime = io ? { io } : null;
51
+ const socketAdmin = setupSocketAdminUI(io);
65
52
 
66
53
  let shuttingDown = false;
67
54
  let broadcastSubClient: ReturnType<typeof redisClientIfReady> | null = null;
@@ -83,7 +70,9 @@ export async function startServer(): Promise<ServerHandle> {
83
70
  }
84
71
  broadcastSubClient = null;
85
72
  }
86
- await Promise.allSettled([closeRealtime(), stopQueueRuntime(), closeHttpServer()]);
73
+
74
+ // closeRealtime is handled by server.close() adapter
75
+ await Promise.allSettled([stopQueueRuntime(), closeHttpServer()]);
87
76
  stopQueueDashboard();
88
77
  await closeRedis();
89
78
 
@@ -111,57 +100,52 @@ export async function startServer(): Promise<ServerHandle> {
111
100
  }
112
101
 
113
102
  const views = new Set(parseCsvOrFallback(process.env.NEXWIRE_DEV_VIEWS, []).map((view) => view.toLowerCase()));
114
- let redisWarnColor: ((text: string) => string) | null = null;
103
+
115
104
  if (appConfig.openApiEnabled) {
116
105
  console.log(`API Docs: ${serverUrl(server, "/api-docs")}`);
117
106
  }
118
107
 
119
108
  if (appConfig.uiEnabled) {
120
- if (process.env.NEXWIRE_FRONTEND_URL) {
121
- console.log(`UI: ${process.env.NEXWIRE_FRONTEND_URL}`);
122
- } else {
123
- console.log("UI enabled");
124
- }
109
+ console.log(`UI: ${process.env.NEXWIRE_FRONTEND_URL}`);
125
110
  } else {
126
- console.log(chalk.gray("UI disabled (disabled in src/config/app.ts)"));
111
+ console.log(chalk.gray("UI: .env -> UI=false (API-only mode)"));
127
112
  }
128
113
 
129
- const bullboardLine = `${bullBoard.enabled ? "Queue Dashboard enabled" : "Queue Dashboard unavailable"}: ${serverUrl(server, bullBoard.basePath)}`;
130
- console.log(redisWarnColor ? redisWarnColor(bullboardLine) : bullboardLine);
114
+ if (redisConfig.enabled && bullBoard.enabled) {
115
+ console.log(`Queue: ${serverUrl(server, bullBoard.basePath)}`);
116
+ }
131
117
 
132
118
  if (views.has("maildev")) {
133
- const maildevLine = `MailDev: http://localhost:${mailConfig.maildev.webPort} (SMTP ${mailConfig.maildev.smtpPort})`;
134
- const line = `${maildevLine} (requested; see dev process status)`;
135
- console.log(redisWarnColor ? redisWarnColor(line) : line);
119
+ console.log(`MailDev: http://localhost:${mailConfig.maildev.webPort} (SMTP ${mailConfig.maildev.smtpPort})`);
136
120
  }
137
121
 
138
122
  if (views.has("studio")) {
139
- console.log("Drizzle Studio requested: https://local.drizzle.studio (see dev process status)");
123
+ console.log("Drizzle: https://local.drizzle.studio");
140
124
  }
141
125
 
142
126
  const wsUrl = serverUrl(server, WS_PATH).replace(/^http/, "ws");
143
- const socketLine = !realtimeConfig.enabled
144
- ? "Realtime (Socket.IO) disabled"
145
- : realtime
146
- ? `Realtime (Socket.IO) enabled: ${socketAdmin.enabled ? `Admin UI: https://admin.socket.io | ${wsUrl}` : `| ${wsUrl}`}`
147
- : "Realtime (Socket.IO) unavailable";
148
- console.log(!realtimeConfig.enabled ? chalk.gray(socketLine) : realtime ? chalk.green(socketLine) : chalk.yellow(socketLine));
149
-
150
- if (views.has("redis")) {
151
- const redisUiLine = `Redis UI: http://localhost:${redisConfig.commanderPort}`;
152
- const line = `${redisUiLine} (requested; see dev process status)`;
153
- console.log(redisWarnColor ? redisWarnColor(line) : line);
127
+ if (!realtimeConfig.enabled) {
128
+ console.log(chalk.gray("Realtime (Socket.IO) disabled"));
129
+ } else if (realtime) {
130
+ if (socketAdmin.enabled) {
131
+ console.log(`Realtime: https://admin.socket.io | ${wsUrl}`);
132
+ } else {
133
+ console.log(`Realtime: ${wsUrl}`);
134
+ }
135
+ } else {
136
+ console.log(chalk.yellow("Realtime (Socket.IO) unavailable"));
137
+ }
138
+
139
+ if (redisConfig.enabled && views.has("redis")) {
140
+ console.log(`Redis UI: http://localhost:${redisConfig.commanderPort}`);
154
141
  }
155
142
 
156
143
  if (!redisConfig.enabled) {
157
- redisWarnColor = chalk.cyan;
158
- console.log(chalk.cyan("Redis disabled (configured in src/config/redis.ts)"));
159
- console.log(chalk.cyan(`Redis-backed services disabled: ${redisBackedServices}`));
144
+ console.log(chalk.cyan(`Redis: .env -> REDIS=false (disabled ${redisBackedServices})`));
160
145
  } else if (redisReady()) {
161
146
  console.log(chalk.green(`Redis connected: ${redisConfig.url}`));
162
147
  console.log(chalk.green(`Redis-backed services enabled: ${redisBackedServices}`));
163
148
  } else {
164
- redisWarnColor = chalk.yellow;
165
149
  console.log(chalk.yellow(`Redis unavailable: ${redisError() || "not connected"}`));
166
150
  console.log(chalk.yellow(`Redis-backed services unavailable: ${redisBackedServices}`));
167
151
  }
@@ -1,8 +1,16 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { NextFunction, Request, Response } from "express";
3
3
  import { appConfig, redisConfig, sessionConfig } from "@/config/index.js";
4
+ import { createCircuitBreaker, isCircuitBreakerError } from "@/framework/circuit-breaker/cb.js";
4
5
  import { redisClientIfReady } from "@/framework/redis/client.js";
5
6
 
7
+ // Initialize the breaker for Session operations
8
+ const sessionBreaker = createCircuitBreaker({
9
+ name: "Redis Session",
10
+ failureThreshold: 3,
11
+ resetTimeout: 30000
12
+ });
13
+
6
14
  function originOf(url?: string) {
7
15
  if (!url) return "";
8
16
  try {
@@ -22,6 +30,9 @@ function sessionKey(id: string) {
22
30
  return `${sessionConfig.keyPrefix}:${id}`;
23
31
  }
24
32
 
33
+ /**
34
+ * Global middleware that ensures every request has a session cookie.
35
+ */
25
36
  export async function sessionMiddleware(req: Request, res: Response, next: NextFunction) {
26
37
  let sessionId = req.cookies?.[sessionConfig.cookieName];
27
38
 
@@ -38,26 +49,44 @@ export async function sessionMiddleware(req: Request, res: Response, next: NextF
38
49
  }
39
50
 
40
51
  res.locals.sessionId = sessionId;
52
+
53
+ // Refresh is safely wrapped in the breaker internally
41
54
  await session.refresh(sessionId);
55
+
42
56
  next();
43
57
  }
44
58
 
59
+ /** Redis-backed server-side session store. */
45
60
  export const session = {
46
61
  async start(data: Record<string, unknown> = {}) {
47
62
  const client = redisClientIfReady();
48
63
  if (!client) return "";
49
64
 
50
- const id = randomUUID();
51
- await client.set(sessionKey(id), JSON.stringify(data), "EX", sessionConfig.ttlSeconds);
52
- return id;
65
+ try {
66
+ return await sessionBreaker.exec(async () => {
67
+ const id = randomUUID();
68
+ await client.set(sessionKey(id), JSON.stringify(data), "EX", sessionConfig.ttlSeconds);
69
+ return id;
70
+ });
71
+ } catch (error) {
72
+ if (isCircuitBreakerError(error)) return "";
73
+ return "";
74
+ }
53
75
  },
54
76
 
55
77
  async all<T = Record<string, unknown>>(id: string): Promise<T | null> {
56
78
  const client = redisClientIfReady();
57
79
  if (!client) return null;
58
80
 
59
- const data = await client.get(sessionKey(id));
60
- return data ? JSON.parse(data) : null;
81
+ try {
82
+ return await sessionBreaker.exec(async () => {
83
+ const data = await client.get(sessionKey(id));
84
+ return data ? (JSON.parse(data) as T) : null;
85
+ });
86
+ } catch (error) {
87
+ if (isCircuitBreakerError(error)) return null;
88
+ return null;
89
+ }
61
90
  },
62
91
 
63
92
  async get<T>(id: string, key: string): Promise<T | null> {
@@ -69,10 +98,22 @@ export const session = {
69
98
  const client = redisClientIfReady();
70
99
  if (!client) return false;
71
100
 
72
- const data = (await session.all<Record<string, unknown>>(id)) || {};
73
- data[key] = value;
74
- await client.set(sessionKey(id), JSON.stringify(data), "EX", sessionConfig.ttlSeconds);
75
- return true;
101
+ try {
102
+ return await sessionBreaker.exec(async () => {
103
+ // Read through the client directly rather than `session.all()`. Calling
104
+ // another breaker-wrapped method from inside `exec` deadlocks the
105
+ // half-open probe: the inner call sees the outer probe in flight and
106
+ // rejects, so a `set` could never be the call that closes the breaker.
107
+ const raw = await client.get(sessionKey(id));
108
+ const data = raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
109
+ data[key] = value;
110
+ await client.set(sessionKey(id), JSON.stringify(data), "EX", sessionConfig.ttlSeconds);
111
+ return true;
112
+ });
113
+ } catch (error) {
114
+ if (isCircuitBreakerError(error)) return false;
115
+ return false;
116
+ }
76
117
  },
77
118
 
78
119
  /** @deprecated Use set() instead. */
@@ -84,19 +125,33 @@ export const session = {
84
125
  const client = redisClientIfReady();
85
126
  if (!client) return false;
86
127
 
87
- await client.expire(sessionKey(id), sessionConfig.ttlSeconds);
88
- return true;
128
+ try {
129
+ return await sessionBreaker.exec(async () => {
130
+ await client.expire(sessionKey(id), sessionConfig.ttlSeconds);
131
+ return true;
132
+ });
133
+ } catch (error) {
134
+ if (isCircuitBreakerError(error)) return false;
135
+ return false;
136
+ }
89
137
  },
90
138
 
91
139
  async destroy(id: string) {
92
140
  const client = redisClientIfReady();
93
141
  if (!client) return false;
94
142
 
95
- await client.del(sessionKey(id));
96
- return true;
143
+ try {
144
+ return await sessionBreaker.exec(async () => {
145
+ await client.del(sessionKey(id));
146
+ return true;
147
+ });
148
+ } catch (error) {
149
+ if (isCircuitBreakerError(error)) return false;
150
+ return false;
151
+ }
97
152
  },
98
153
 
99
154
  isAvailable() {
100
- return redisConfig.enabled && redisClientIfReady() !== null;
155
+ return redisConfig.enabled && redisClientIfReady() !== null && sessionBreaker.getState() !== "open";
101
156
  }
102
157
  };