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,119 +1,149 @@
1
- import { createAdapter } from "@socket.io/redis-adapter";
2
- import { Server as SocketIOServer } from "socket.io";
3
- import { appConfig, realtimeConfig, redisConfig } from "@/config/index.js";
4
- import { authFromSocketHandshake, unauthenticatedRealtimeAuth } from "@/framework/realtime/socket-cookie.js";
5
- import type { RealtimeAuthContext } from "@/framework/realtime/types.js";
6
- import { redisClientIfReady } from "@/framework/redis/client.js";
7
- import { logger } from "@/framework/support/logger.js";
8
-
9
- export type RealtimeServeWebSocket = Record<string, never>;
10
-
11
- export type RealtimeInit = {
12
- io: SocketIOServer;
13
- websocketServe: RealtimeServeWebSocket;
14
- };
15
-
16
- export const WS_PATH = "/socket.io";
17
-
18
- let io: SocketIOServer | null = null;
19
- let adapterSubClient: ReturnType<typeof redisClientIfReady> | null = null;
20
-
21
- function socketAllowedOrigins() {
22
- const origins = [appConfig.url, appConfig.frontendUrl].filter(Boolean) as string[];
23
- if (realtimeConfig.enabled) origins.push("https://admin.socket.io");
24
- return [...new Set(origins)];
25
- }
26
-
27
- async function attachRedisAdapter() {
28
- if (!redisConfig.enabled) return;
29
-
30
- const pubClient = redisClientIfReady();
31
- if (!pubClient) return;
32
-
33
- adapterSubClient = pubClient.duplicate();
34
- await adapterSubClient.connect();
35
- io?.adapter(createAdapter(pubClient, adapterSubClient));
36
- }
37
-
38
- function registerAuthAndRooms() {
39
- io?.use(async (socket, next) => {
40
- const auth = await authFromSocketHandshake(socket);
41
- socket.data.auth = auth as RealtimeAuthContext;
42
-
43
- if (auth.isAuthenticated) {
44
- socket.join("auth");
45
- if (auth.userId) socket.join(`user:${auth.userId}`);
46
- for (const role of auth.roles) socket.join(`role:${role}`);
47
- } else {
48
- socket.join("guest");
49
- }
50
-
51
- next();
52
- });
53
-
54
- io?.on("connection", (socket) => {
55
- const auth = (socket.data.auth || unauthenticatedRealtimeAuth()) as RealtimeAuthContext;
56
-
57
- logger.debug("Socket connected", {
58
- socketId: socket.id,
59
- authenticated: auth.isAuthenticated,
60
- userId: auth.userId
61
- });
62
-
63
- socket.on("join", (room) => socket.join(String(room)));
64
- socket.on("disconnect", (reason) => {
65
- logger.info("Socket disconnected", {
66
- socketId: socket.id,
67
- reason,
68
- authenticated: auth.isAuthenticated,
69
- userId: auth.userId
70
- });
71
- });
72
- });
73
- }
74
-
75
- export async function initRealtime(target: { httpServer: any }): Promise<RealtimeInit | null> {
76
- if (!realtimeConfig.enabled) {
77
- logger.info("Socket.IO disabled (disabled in config)");
78
- return null;
79
- }
80
-
81
- if (io) return { io, websocketServe: {} };
82
-
83
- io = new SocketIOServer(target.httpServer, {
84
- cors: { origin: socketAllowedOrigins(), credentials: true }
85
- });
86
-
87
- await attachRedisAdapter();
88
- registerAuthAndRooms();
89
-
90
- logger.info(`Socket.IO enabled at ${WS_PATH}`);
91
- return { io, websocketServe: {} };
92
- }
93
-
94
- export function socketServer() {
95
- return io;
96
- }
97
-
98
- export function ioServer() {
99
- return io;
100
- }
101
-
102
- export async function closeRealtime() {
103
- if (!io) return;
104
-
105
- await new Promise<void>((resolve) => {
106
- io?.close(() => resolve());
107
- });
108
-
109
- if (adapterSubClient) {
110
- try {
111
- await adapterSubClient.quit();
112
- } catch {
113
- adapterSubClient.disconnect();
114
- }
115
- adapterSubClient = null;
116
- }
117
-
118
- io = null;
119
- }
1
+ import { createAdapter } from "@socket.io/redis-adapter";
2
+ import { Server as SocketIOServer } from "socket.io";
3
+ import { appConfig, realtimeConfig, redisConfig } from "@/config/index.js";
4
+ import { createCircuitBreaker, isCircuitBreakerError } from "@/framework/circuit-breaker/cb.js";
5
+ import { authFromSocketHandshake, unauthenticatedRealtimeAuth } from "@/framework/realtime/socket-cookie.js";
6
+ import type { RealtimeAuthContext } from "@/framework/realtime/types.js";
7
+ import { redisClientIfReady } from "@/framework/redis/client.js";
8
+ import { logger } from "@/framework/support/logger.js";
9
+
10
+ export const WS_PATH = "/socket.io";
11
+
12
+ // Create a breaker for the Socket.IO Redis adapter
13
+ const socketAdapterBreaker = createCircuitBreaker({
14
+ name: "Socket.IO Redis Adapter",
15
+ failureThreshold: 3,
16
+ resetTimeout: 30000
17
+ });
18
+
19
+ export type RealtimeServeWebSocket = Record<string, never>;
20
+
21
+ export type RealtimeInit = {
22
+ io: SocketIOServer;
23
+ websocketServe: RealtimeServeWebSocket;
24
+ };
25
+
26
+ let io: SocketIOServer | null = null;
27
+ let adapterSubClient: ReturnType<typeof redisClientIfReady> | null = null;
28
+
29
+ function socketAllowedOrigins() {
30
+ const origins = [appConfig.url, appConfig.frontendUrl].filter(Boolean) as string[];
31
+ if (realtimeConfig.enabled) origins.push("https://admin.socket.io");
32
+ return [...new Set(origins)];
33
+ }
34
+
35
+ async function attachRedisAdapter() {
36
+ if (!redisConfig.enabled) return;
37
+
38
+ try {
39
+ await socketAdapterBreaker.exec(async () => {
40
+ const pubClient = redisClientIfReady();
41
+ if (!pubClient) throw new Error("Redis client not ready");
42
+
43
+ // Socket.IO requires a duplicate connection for subscriptions
44
+ adapterSubClient = pubClient.duplicate();
45
+ await adapterSubClient.connect();
46
+
47
+ io?.adapter(createAdapter(pubClient, adapterSubClient));
48
+ logger.debug("Socket.IO Redis adapter attached successfully");
49
+ });
50
+ } catch (error) {
51
+ if (isCircuitBreakerError(error)) {
52
+ logger.warn("Socket.IO Redis adapter skipped due to circuit breaker");
53
+ } else {
54
+ logger.error("Failed to attach Socket.IO Redis adapter", { error });
55
+ }
56
+ // Fallback: Socket.IO will automatically use its default in-memory adapter.
57
+ }
58
+ }
59
+
60
+ function registerAuthAndRooms() {
61
+ io?.use(async (socket, next) => {
62
+ const auth = await authFromSocketHandshake(socket);
63
+ socket.data.auth = auth as RealtimeAuthContext;
64
+
65
+ if (auth.isAuthenticated) {
66
+ socket.join("auth");
67
+ if (auth.userId) socket.join(`user:${auth.userId}`);
68
+ for (const role of auth.roles) socket.join(`role:${role}`);
69
+ } else {
70
+ socket.join("guest");
71
+ }
72
+
73
+ next();
74
+ });
75
+
76
+ io?.on("connection", (socket) => {
77
+ const auth = (socket.data.auth || unauthenticatedRealtimeAuth()) as RealtimeAuthContext;
78
+
79
+ logger.debug("Socket connected", {
80
+ socketId: socket.id,
81
+ authenticated: auth.isAuthenticated,
82
+ userId: auth.userId
83
+ });
84
+
85
+ socket.on("join", (room) => socket.join(String(room)));
86
+ socket.on("disconnect", (reason) => {
87
+ logger.info("Socket disconnected", {
88
+ socketId: socket.id,
89
+ reason,
90
+ authenticated: auth.isAuthenticated,
91
+ userId: auth.userId
92
+ });
93
+ });
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Why: Boots the singleton Socket.IO server for Express.
99
+ * When: Express HTTP server starts and realtime is enabled.
100
+ * Where: Framework server lifecycle.
101
+ * How: Attaches Socket.IO directly to the Express/Node httpServer.
102
+ */
103
+ export async function initRealtime(target: { httpServer: any }): Promise<RealtimeInit | null> {
104
+ if (!realtimeConfig.enabled) {
105
+ logger.debug("Socket.IO disabled (disabled in config)");
106
+ return null;
107
+ }
108
+
109
+ if (io) return { io, websocketServe: {} };
110
+
111
+ // Express uses the native Node.js httpServer
112
+ io = new SocketIOServer(target.httpServer, {
113
+ path: WS_PATH, // Explicitly set the path to match Hono
114
+ cors: { origin: socketAllowedOrigins(), credentials: true }
115
+ });
116
+
117
+ await attachRedisAdapter();
118
+ registerAuthAndRooms();
119
+
120
+ logger.debug(`Socket.IO enabled at ${WS_PATH}`);
121
+ return { io, websocketServe: {} };
122
+ }
123
+
124
+ export function socketServer() {
125
+ return io;
126
+ }
127
+
128
+ export function ioServer() {
129
+ return io;
130
+ }
131
+
132
+ export async function closeRealtime() {
133
+ if (!io) return;
134
+
135
+ await new Promise<void>((resolve) => {
136
+ io?.close(() => resolve());
137
+ });
138
+
139
+ if (adapterSubClient) {
140
+ try {
141
+ await adapterSubClient.quit();
142
+ } catch {
143
+ adapterSubClient.disconnect();
144
+ }
145
+ adapterSubClient = null;
146
+ }
147
+
148
+ io = null;
149
+ }
@@ -1,151 +1,148 @@
1
- import { Redis } from "ioredis";
2
- import { redisConfig } from "@/config/index.js";
3
- import { CircuitBreaker } from "@/framework/circuit-breaker/cb.js";
4
- import { logger } from "@/framework/support/logger.js";
5
-
6
- export type redis = Redis;
7
-
8
- let client: redis | null = null;
9
- let ready = false;
10
- let lastError: string | null = null;
11
- let redisBreaker: CircuitBreaker | null = null;
12
-
13
- if (redisConfig.enabled) {
14
- redisBreaker = new CircuitBreaker();
15
- }
16
-
17
- function safeRedisUrl() {
18
- try {
19
- const url = new URL(redisConfig.url);
20
- if (url.password) url.password = "***";
21
- return url.toString();
22
- } catch {
23
- return redisConfig.url;
24
- }
25
- }
26
-
27
- /**
28
- * Why: Initializes shared Redis connection for cache/queue/events/session.
29
- * When: Runtime bootstrap before Redis-backed features are used.
30
- * Where: Server, worker, scheduler startup paths.
31
- * How: Connects once, tracks readiness, and degrades gracefully if unavailable.
32
- */
33
- export async function initRedis() {
34
- if (client) return client;
35
-
36
- if (!redisConfig.enabled) {
37
- ready = false;
38
- lastError = "redis disabled in config";
39
- logger.info("Redis disabled; cache, session storage, queue, events, and cockpit unavailable");
40
- return null;
41
- }
42
-
43
- if (redisBreaker?.getState() === "open") {
44
- lastError = "redis circuit breaker is open";
45
- logger.warn("Redis circuit breaker is open; skipping connection");
46
- return null;
47
- }
48
-
49
- try {
50
- client = new Redis(redisConfig.url, {
51
- lazyConnect: true,
52
- enableReadyCheck: true,
53
- maxRetriesPerRequest: 0,
54
- retryStrategy: () => null
55
- });
56
-
57
- client.on("error", (error) => {
58
- lastError = error.message;
59
- if (ready) {
60
- logger.warn("Redis connection error", {
61
- redisUrl: safeRedisUrl(),
62
- error: error.message
63
- });
64
- }
65
- });
66
-
67
- await client.connect();
68
- await client.ping();
69
- ready = true;
70
- redisBreaker?.onSuccess();
71
- return client;
72
- } catch (error) {
73
- lastError ||= error instanceof Error ? error.message : String(error);
74
- client?.disconnect();
75
- client = null;
76
- ready = false;
77
- redisBreaker?.onFailure();
78
- logger.warn(`Redis unavailable; continuing without Redis features (${lastError})`);
79
- return null;
80
- }
81
- }
82
-
83
- /**
84
- * Why: Returns the current Redis client instance.
85
- * When: Internal helpers need direct Redis operations.
86
- * Where: Cache/session/queue/events modules.
87
- * How: Returns nullable singleton without creating a new connection.
88
- */
89
- export function redis() {
90
- return client;
91
- }
92
-
93
- /**
94
- * Why: Indicates whether Redis is connected and usable.
95
- * When: Features need to branch on Redis availability.
96
- * Where: Cache/session/queue/event internals.
97
- * How: Combines readiness flag with client presence.
98
- */
99
- export function redisReady() {
100
- return ready && client !== null;
101
- }
102
-
103
- /**
104
- * Why: Provides a single ready-checked Redis client accessor.
105
- * When: Redis-backed modules need quick availability guard.
106
- * Where: Cache/session/queue/events/BullBoard internals.
107
- * How: Returns connected client or null when Redis is unavailable.
108
- */
109
- export function redisClientIfReady() {
110
- if (redisBreaker?.getState() === "open") return null;
111
- return redisReady() ? client : null;
112
- }
113
-
114
- /**
115
- * Why: Exposes last Redis error for diagnostics.
116
- * When: Startup or health logs need failure detail.
117
- * Where: Runtime status output.
118
- * How: Returns cached error message string.
119
- */
120
- export function redisError() {
121
- return lastError;
122
- }
123
-
124
- /**
125
- * Why: Returns the Redis circuit breaker instance or null when disabled.
126
- * When: Health checks and diagnostics need breaker state.
127
- * Where: Runtime status and monitoring paths.
128
- * How: Returns the breaker if Redis is enabled, null otherwise.
129
- */
130
- export function getRedisBreaker() {
131
- return redisBreaker;
132
- }
133
-
134
- /**
135
- * Why: Closes Redis connection during graceful shutdown.
136
- * When: Process stop hooks for server/worker/scheduler.
137
- * Where: Runtime lifecycle files.
138
- * How: Attempts quit, falls back to disconnect, then resets state.
139
- */
140
- export async function closeRedis() {
141
- if (!client) return;
142
-
143
- try {
144
- await client.quit();
145
- } catch {
146
- client.disconnect();
147
- } finally {
148
- client = null;
149
- ready = false;
150
- }
151
- }
1
+ import { Redis } from "ioredis";
2
+ import { redisConfig } from "@/config/index.js";
3
+ import { createCircuitBreaker, isCircuitBreakerError } from "@/framework/circuit-breaker/cb.js";
4
+ import { logger } from "@/framework/support/logger.js";
5
+
6
+ export type redis = Redis;
7
+
8
+ let client: redis | null = null;
9
+ let ready = false;
10
+ let lastError: string | null = null;
11
+ let redisBreaker: any | null = null;
12
+
13
+ if (redisConfig.enabled) {
14
+ redisBreaker = createCircuitBreaker({ name: "Redis", failureThreshold: 3, resetTimeout: 30000 });
15
+ }
16
+
17
+ function safeRedisUrl() {
18
+ try {
19
+ const url = new URL(redisConfig.url);
20
+ if (url.password) url.password = "***";
21
+ return url.toString();
22
+ } catch {
23
+ return redisConfig.url;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Why: Initializes shared Redis connection for cache/queue/events/session.
29
+ * When: Runtime bootstrap before Redis-backed features are used.
30
+ * Where: Server, worker, scheduler startup paths.
31
+ * How: Connects once, tracks readiness, and degrades gracefully if unavailable.
32
+ */
33
+ export async function initRedis() {
34
+ if (client) return client;
35
+
36
+ if (!redisConfig.enabled) {
37
+ ready = false;
38
+ lastError = "redis disabled in config";
39
+ logger.debug("Redis disabled; cache, session storage, queue, events, and cockpit unavailable");
40
+ return null;
41
+ }
42
+
43
+ try {
44
+ return await redisBreaker.exec(async () => {
45
+ client = new Redis(redisConfig.url, {
46
+ lazyConnect: true,
47
+ enableReadyCheck: true,
48
+ maxRetriesPerRequest: 0,
49
+ retryStrategy: () => null
50
+ });
51
+
52
+ client.on("error", (error) => {
53
+ lastError = error.message;
54
+ if (ready) logger.warn("Redis connection error", { redisUrl: safeRedisUrl(), error: error.message });
55
+ });
56
+
57
+ await client.connect();
58
+ await client.ping();
59
+
60
+ ready = true;
61
+ logger.debug("Redis connected successfully");
62
+ return client;
63
+ });
64
+ } catch (error) {
65
+ // Use the functional guard
66
+ if (isCircuitBreakerError(error)) {
67
+ lastError = error.message;
68
+ logger.warn("Redis initialization blocked by circuit breaker");
69
+ } else {
70
+ lastError = error instanceof Error ? error.message : String(error);
71
+ logger.warn(`Redis unavailable; continuing without Redis features (${lastError})`);
72
+ }
73
+
74
+ client?.disconnect();
75
+ client = null;
76
+ ready = false;
77
+ return null;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Why: Returns the current Redis client instance.
83
+ * When: Internal helpers need direct Redis operations.
84
+ * Where: Cache/session/queue/events modules.
85
+ * How: Returns nullable singleton without creating a new connection.
86
+ */
87
+ export function redis() {
88
+ return client;
89
+ }
90
+
91
+ /**
92
+ * Why: Indicates whether Redis is connected and usable.
93
+ * When: Features need to branch on Redis availability.
94
+ * Where: Cache/session/queue/event internals.
95
+ * How: Combines readiness flag with client presence.
96
+ */
97
+ export function redisReady() {
98
+ return ready && client !== null;
99
+ }
100
+
101
+ /**
102
+ * Why: Provides a single ready-checked Redis client accessor.
103
+ * When: Redis-backed modules need quick availability guard.
104
+ * Where: Cache/session/queue/events/BullBoard internals.
105
+ * How: Returns connected client or null when Redis is unavailable.
106
+ */
107
+ export function redisClientIfReady() {
108
+ return redisReady() ? client : null;
109
+ }
110
+
111
+ /**
112
+ * Why: Exposes last Redis error for diagnostics.
113
+ * When: Startup or health logs need failure detail.
114
+ * Where: Runtime status output.
115
+ * How: Returns cached error message string.
116
+ */
117
+ export function redisError() {
118
+ return lastError;
119
+ }
120
+
121
+ /**
122
+ * Why: Returns the Redis circuit breaker instance or null when disabled.
123
+ * When: Health checks and diagnostics need breaker state.
124
+ * Where: Runtime status and monitoring paths.
125
+ * How: Returns the breaker if Redis is enabled, null otherwise.
126
+ */
127
+ export function getRedisBreaker() {
128
+ return redisBreaker;
129
+ }
130
+
131
+ /**
132
+ * Why: Closes Redis connection during graceful shutdown.
133
+ * When: Process stop hooks for server/worker/scheduler.
134
+ * Where: Runtime lifecycle files.
135
+ * How: Attempts quit, falls back to disconnect, then resets state.
136
+ */
137
+ export async function closeRedis() {
138
+ if (!client) return;
139
+
140
+ try {
141
+ await client.quit();
142
+ } catch {
143
+ client.disconnect();
144
+ } finally {
145
+ client = null;
146
+ ready = false;
147
+ }
148
+ }
@@ -1,34 +1,39 @@
1
- import type { AddressInfo } from "node:net";
2
- import type { Express } from "express";
3
- import type { RealtimeServeWebSocket } from "@/framework/realtime/socket.js";
4
- import { runtime } from "@/framework/runtime/runtime.js";
5
-
6
- export type HttpServerHandle = {
7
- address(): string | AddressInfo | null;
8
- close(): Promise<void>;
9
- native: unknown;
10
- };
11
-
12
- async function startNode(app: Express, port: number, _websocket?: RealtimeServeWebSocket): Promise<HttpServerHandle> {
13
- const { createServer } = await import("node:http");
14
-
15
- const server = createServer(app);
16
- await new Promise<void>((resolve) => server.listen(port, resolve));
17
-
18
- return {
19
- native: server,
20
- address: () => server.address(),
21
- close: () => new Promise<void>((resolve) => server.close(() => resolve()))
22
- };
23
- }
24
-
25
- async function startBun(app: Express, port: number, _websocket?: RealtimeServeWebSocket): Promise<HttpServerHandle> {
26
- const server = await startNode(app, port);
27
- return server;
28
- }
29
-
30
- export async function startHttpServer(app: Express, port: number, websocket?: RealtimeServeWebSocket): Promise<HttpServerHandle> {
31
- const activeRuntime = runtime();
32
- if (activeRuntime === "bun") return startBun(app, port, websocket);
33
- return startNode(app, port, websocket);
34
- }
1
+ import { createServer, type Server } from "node:http";
2
+ import type { AddressInfo } from "node:net";
3
+ import type { Express } from "express";
4
+ import { closeRealtime, initRealtime } from "@/framework/realtime/socket.js";
5
+
6
+ export type HttpServerHandle = {
7
+ address(): string | AddressInfo | null;
8
+ close(): Promise<void>;
9
+ native: unknown;
10
+ };
11
+
12
+ async function startNode(app: Express, port: number): Promise<HttpServerHandle> {
13
+ const server = createServer(app);
14
+
15
+ // 1. Attach Socket.IO BEFORE starting the server
16
+ await initRealtime({ httpServer: server });
17
+
18
+ // 2. Start listening
19
+ await new Promise<void>((resolve, reject) => {
20
+ server.listen(port, () => resolve());
21
+ server.on("error", (err) => reject(err));
22
+ });
23
+
24
+ return {
25
+ native: server,
26
+ address: () => server.address(),
27
+ close: async () => {
28
+ // 3. Close Socket.IO safely, then the HTTP server
29
+ await closeRealtime();
30
+ await new Promise<void>((resolve) => server.close(() => resolve()));
31
+ }
32
+ };
33
+ }
34
+
35
+ export async function startHttpServer(app: Express, port: number): Promise<HttpServerHandle> {
36
+ // Express runs natively on Node.js.
37
+ // Even if you run Express on Bun, Bun's Node compatibility layer handles http.createServer.
38
+ return startNode(app, port);
39
+ }