arcway 0.1.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 (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +711 -0
  3. package/client/env.js +55 -0
  4. package/client/fetcher.js +50 -0
  5. package/client/graphql.js +35 -0
  6. package/client/head.js +140 -0
  7. package/client/hooks/use-api.js +80 -0
  8. package/client/hooks/use-debounce.js +12 -0
  9. package/client/hooks/use-form.js +86 -0
  10. package/client/hooks/use-graphql.js +30 -0
  11. package/client/hooks/use-interval.js +12 -0
  12. package/client/hooks/use-mutation.js +27 -0
  13. package/client/hooks/use-query.js +45 -0
  14. package/client/hooks/web/use-click-outside.js +22 -0
  15. package/client/hooks/web/use-local-storage.js +42 -0
  16. package/client/index.js +62 -0
  17. package/client/page-loader.js +155 -0
  18. package/client/provider.js +53 -0
  19. package/client/query.js +13 -0
  20. package/client/router.jsx +303 -0
  21. package/client/ui/accordion.jsx +65 -0
  22. package/client/ui/accordion.stories.jsx +48 -0
  23. package/client/ui/alert-dialog.jsx +122 -0
  24. package/client/ui/alert-dialog.stories.jsx +44 -0
  25. package/client/ui/alert.jsx +52 -0
  26. package/client/ui/alert.stories.jsx +31 -0
  27. package/client/ui/app-shell.jsx +39 -0
  28. package/client/ui/app-shell.stories.jsx +51 -0
  29. package/client/ui/aspect-ratio.jsx +6 -0
  30. package/client/ui/aspect-ratio.stories.jsx +69 -0
  31. package/client/ui/avatar.jsx +78 -0
  32. package/client/ui/avatar.stories.jsx +62 -0
  33. package/client/ui/badge.jsx +34 -0
  34. package/client/ui/badge.stories.js +32 -0
  35. package/client/ui/breadcrumb.jsx +86 -0
  36. package/client/ui/breadcrumb.stories.jsx +43 -0
  37. package/client/ui/button-group.jsx +58 -0
  38. package/client/ui/button-group.stories.jsx +67 -0
  39. package/client/ui/button.jsx +46 -0
  40. package/client/ui/button.stories.js +72 -0
  41. package/client/ui/calendar.jsx +172 -0
  42. package/client/ui/card.jsx +57 -0
  43. package/client/ui/card.stories.jsx +33 -0
  44. package/client/ui/carousel.jsx +167 -0
  45. package/client/ui/chart.jsx +244 -0
  46. package/client/ui/checkbox.jsx +24 -0
  47. package/client/ui/checkbox.stories.js +33 -0
  48. package/client/ui/collapsible.jsx +12 -0
  49. package/client/ui/collapsible.stories.jsx +42 -0
  50. package/client/ui/combobox.jsx +223 -0
  51. package/client/ui/command.jsx +128 -0
  52. package/client/ui/context-menu.jsx +170 -0
  53. package/client/ui/context-menu.stories.jsx +35 -0
  54. package/client/ui/dialog.jsx +109 -0
  55. package/client/ui/dialog.stories.jsx +37 -0
  56. package/client/ui/direction.jsx +9 -0
  57. package/client/ui/drawer.jsx +87 -0
  58. package/client/ui/dropdown-menu.jsx +172 -0
  59. package/client/ui/dropdown-menu.stories.jsx +34 -0
  60. package/client/ui/empty.jsx +76 -0
  61. package/client/ui/empty.stories.jsx +64 -0
  62. package/client/ui/field.jsx +174 -0
  63. package/client/ui/field.stories.jsx +118 -0
  64. package/client/ui/form.jsx +17 -0
  65. package/client/ui/hooks/use-mobile.js +16 -0
  66. package/client/ui/hover-card.jsx +26 -0
  67. package/client/ui/hover-card.stories.jsx +28 -0
  68. package/client/ui/index.js +649 -0
  69. package/client/ui/input-group.jsx +116 -0
  70. package/client/ui/input-group.stories.jsx +65 -0
  71. package/client/ui/input-otp.jsx +62 -0
  72. package/client/ui/input.jsx +16 -0
  73. package/client/ui/input.stories.js +27 -0
  74. package/client/ui/item.jsx +155 -0
  75. package/client/ui/item.stories.jsx +118 -0
  76. package/client/ui/kbd.jsx +24 -0
  77. package/client/ui/kbd.stories.jsx +32 -0
  78. package/client/ui/label.jsx +16 -0
  79. package/client/ui/label.stories.js +25 -0
  80. package/client/ui/lib/utils.js +6 -0
  81. package/client/ui/main-content.jsx +30 -0
  82. package/client/ui/menubar.jsx +189 -0
  83. package/client/ui/menubar.stories.jsx +43 -0
  84. package/client/ui/native-select.jsx +34 -0
  85. package/client/ui/native-select.stories.jsx +67 -0
  86. package/client/ui/navigation-menu.jsx +120 -0
  87. package/client/ui/navigation-menu.stories.jsx +45 -0
  88. package/client/ui/pagination.jsx +92 -0
  89. package/client/ui/pagination.stories.jsx +52 -0
  90. package/client/ui/panel.jsx +66 -0
  91. package/client/ui/popover.jsx +54 -0
  92. package/client/ui/popover.stories.jsx +27 -0
  93. package/client/ui/progress.jsx +19 -0
  94. package/client/ui/progress.stories.js +34 -0
  95. package/client/ui/radio-group.jsx +33 -0
  96. package/client/ui/radio-group.stories.jsx +49 -0
  97. package/client/ui/resizable.jsx +33 -0
  98. package/client/ui/scroll-area.jsx +41 -0
  99. package/client/ui/scroll-area.stories.jsx +43 -0
  100. package/client/ui/select.jsx +145 -0
  101. package/client/ui/select.stories.jsx +80 -0
  102. package/client/ui/separator.jsx +18 -0
  103. package/client/ui/separator.stories.jsx +37 -0
  104. package/client/ui/sheet.jsx +95 -0
  105. package/client/ui/sheet.stories.jsx +56 -0
  106. package/client/ui/sidebar.jsx +544 -0
  107. package/client/ui/skeleton.jsx +8 -0
  108. package/client/ui/skeleton.stories.js +23 -0
  109. package/client/ui/slider.jsx +41 -0
  110. package/client/ui/slider.stories.js +31 -0
  111. package/client/ui/sonner.jsx +37 -0
  112. package/client/ui/spinner.jsx +14 -0
  113. package/client/ui/spinner.stories.js +16 -0
  114. package/client/ui/style-mira.css +1316 -0
  115. package/client/ui/switch.jsx +22 -0
  116. package/client/ui/switch.stories.js +44 -0
  117. package/client/ui/table.jsx +33 -0
  118. package/client/ui/table.stories.jsx +42 -0
  119. package/client/ui/tabs.jsx +63 -0
  120. package/client/ui/tabs.stories.jsx +45 -0
  121. package/client/ui/textarea.jsx +15 -0
  122. package/client/ui/textarea.stories.js +33 -0
  123. package/client/ui/theme.css +459 -0
  124. package/client/ui/toggle-group.jsx +62 -0
  125. package/client/ui/toggle-group.stories.jsx +68 -0
  126. package/client/ui/toggle.jsx +34 -0
  127. package/client/ui/toggle.stories.js +46 -0
  128. package/client/ui/tooltip.jsx +37 -0
  129. package/client/ui/tooltip.stories.jsx +32 -0
  130. package/client/ui/use-transition.js +35 -0
  131. package/client/ws.js +132 -0
  132. package/package.json +134 -0
  133. package/server/bin/cli.js +42 -0
  134. package/server/bin/commands/build.js +23 -0
  135. package/server/bin/commands/dev.js +57 -0
  136. package/server/bin/commands/docs.js +30 -0
  137. package/server/bin/commands/graphql-schema.js +32 -0
  138. package/server/bin/commands/lint.js +35 -0
  139. package/server/bin/commands/mcp.js +26 -0
  140. package/server/bin/commands/migrate.js +82 -0
  141. package/server/bin/commands/schema.js +41 -0
  142. package/server/bin/commands/seed.js +36 -0
  143. package/server/bin/commands/start.js +31 -0
  144. package/server/bin/commands/test.js +20 -0
  145. package/server/bin/solo.js +4 -0
  146. package/server/boot/index.js +150 -0
  147. package/server/boot.js +2 -0
  148. package/server/build.js +23 -0
  149. package/server/cache/drivers/memory.js +23 -0
  150. package/server/cache/drivers/redis.js +28 -0
  151. package/server/cache/index.js +69 -0
  152. package/server/config/loader.js +89 -0
  153. package/server/config/modules/api.js +17 -0
  154. package/server/config/modules/build.js +9 -0
  155. package/server/config/modules/cache.js +10 -0
  156. package/server/config/modules/database.js +29 -0
  157. package/server/config/modules/events.js +15 -0
  158. package/server/config/modules/files.js +15 -0
  159. package/server/config/modules/jobs.js +20 -0
  160. package/server/config/modules/logger.js +9 -0
  161. package/server/config/modules/mail.js +11 -0
  162. package/server/config/modules/mcp.js +9 -0
  163. package/server/config/modules/pages.js +20 -0
  164. package/server/config/modules/queue.js +10 -0
  165. package/server/config/modules/redis.js +9 -0
  166. package/server/config/modules/server.js +30 -0
  167. package/server/config/modules/session.js +9 -0
  168. package/server/config/modules/websocket.js +11 -0
  169. package/server/constants.js +67 -0
  170. package/server/context.js +15 -0
  171. package/server/db/index.js +87 -0
  172. package/server/db/schema/drivers/mysql.js +28 -0
  173. package/server/db/schema/drivers/pg.js +34 -0
  174. package/server/db/schema/drivers/sqlite.js +22 -0
  175. package/server/db/schema/index.js +78 -0
  176. package/server/db/seeds.js +22 -0
  177. package/server/discovery.js +67 -0
  178. package/server/docs/openapi.js +153 -0
  179. package/server/env.js +17 -0
  180. package/server/events/drivers/memory.js +45 -0
  181. package/server/events/drivers/redis.js +64 -0
  182. package/server/events/handler.js +67 -0
  183. package/server/events/index.js +35 -0
  184. package/server/events/pattern.js +5 -0
  185. package/server/files/drivers/local.js +83 -0
  186. package/server/files/drivers/s3.js +113 -0
  187. package/server/files/index.js +57 -0
  188. package/server/filewatcher/index.js +156 -0
  189. package/server/glob.js +6 -0
  190. package/server/graphql/discovery.js +70 -0
  191. package/server/graphql/handler.js +41 -0
  192. package/server/graphql/index.js +13 -0
  193. package/server/graphql/loaders.js +19 -0
  194. package/server/graphql/merge.js +48 -0
  195. package/server/graphql/subscriptions.js +43 -0
  196. package/server/health.js +34 -0
  197. package/server/helpers.js +9 -0
  198. package/server/index.js +55 -0
  199. package/server/internals.js +139 -0
  200. package/server/jobs/cron.js +10 -0
  201. package/server/jobs/drivers/knex-queue.js +207 -0
  202. package/server/jobs/drivers/lease.js +148 -0
  203. package/server/jobs/drivers/memory-queue.js +134 -0
  204. package/server/jobs/queue.js +27 -0
  205. package/server/jobs/runner.js +197 -0
  206. package/server/jobs/throughput.js +63 -0
  207. package/server/lib/vault/encrypt.js +40 -0
  208. package/server/lib/vault/ids.js +9 -0
  209. package/server/lib/vault/index.js +14 -0
  210. package/server/lib/vault/jwt.js +55 -0
  211. package/server/lib/vault/password.js +10 -0
  212. package/server/lint/boundaries.js +77 -0
  213. package/server/logger/index.js +130 -0
  214. package/server/mail/drivers/console.js +31 -0
  215. package/server/mail/drivers/smtp.js +34 -0
  216. package/server/mail/imap.js +105 -0
  217. package/server/mail/inbound-store.js +58 -0
  218. package/server/mail/inbound.js +79 -0
  219. package/server/mail/index.js +112 -0
  220. package/server/mcp/debug-api.js +137 -0
  221. package/server/mcp/helpers.js +30 -0
  222. package/server/mcp/index.js +77 -0
  223. package/server/mcp/runtime.js +7 -0
  224. package/server/mcp/server.js +19 -0
  225. package/server/mcp/tools/debugging.js +133 -0
  226. package/server/mcp/tools/introspection.js +87 -0
  227. package/server/middlewares/cors.js +30 -0
  228. package/server/middlewares/index.js +3 -0
  229. package/server/middlewares/require-session.js +15 -0
  230. package/server/module-loader.js +9 -0
  231. package/server/pages/build-client.js +187 -0
  232. package/server/pages/build-css.js +47 -0
  233. package/server/pages/build-manifest.js +55 -0
  234. package/server/pages/build-plugins.js +75 -0
  235. package/server/pages/build-server.js +115 -0
  236. package/server/pages/build.js +116 -0
  237. package/server/pages/discovery.js +120 -0
  238. package/server/pages/fonts.js +128 -0
  239. package/server/pages/handler.js +276 -0
  240. package/server/pages/hmr.js +176 -0
  241. package/server/pages/pages-router.js +78 -0
  242. package/server/pages/ssr.js +276 -0
  243. package/server/pages/static.js +92 -0
  244. package/server/pages/watcher.js +90 -0
  245. package/server/queue/drivers/knex.js +67 -0
  246. package/server/queue/drivers/redis.js +91 -0
  247. package/server/queue/index.js +61 -0
  248. package/server/rate-limit/consume.js +21 -0
  249. package/server/rate-limit/drivers/memory.js +24 -0
  250. package/server/rate-limit/drivers/redis.js +32 -0
  251. package/server/rate-limit/index.js +33 -0
  252. package/server/redis/index.js +67 -0
  253. package/server/ring-buffer.js +44 -0
  254. package/server/route.js +4 -0
  255. package/server/router/api-router.js +317 -0
  256. package/server/router/cors.js +31 -0
  257. package/server/router/middleware.js +91 -0
  258. package/server/router/routes.js +132 -0
  259. package/server/server.js +35 -0
  260. package/server/session/helpers.js +21 -0
  261. package/server/session/index.js +89 -0
  262. package/server/static/index.js +36 -0
  263. package/server/system-jobs/index.js +50 -0
  264. package/server/system-routes/index.js +84 -0
  265. package/server/testing/index.js +263 -0
  266. package/server/validation.js +41 -0
  267. package/server/watcher.js +34 -0
  268. package/server/web-server.js +231 -0
  269. package/server/ws/discovery.js +54 -0
  270. package/server/ws/index.js +14 -0
  271. package/server/ws/realtime.js +318 -0
  272. package/server/ws/registry.js +17 -0
  273. package/server/ws/server.js +152 -0
  274. package/server/ws/ws-router.js +335 -0
@@ -0,0 +1,335 @@
1
+ import { Server as SocketIOServer } from 'socket.io';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { registerWsServer, unregisterWsServer } from './registry.js';
4
+ import { parseCookies, resolveSession, flattenHeaders } from '../session/helpers.js';
5
+ import { toErrorMessage } from '../helpers.js';
6
+
7
+ class WsRouter {
8
+ _apiRouter;
9
+ _log;
10
+ _pingIntervalMs;
11
+ _io = null;
12
+ _clients = new Map();
13
+ _clientsBySocketId = new Map();
14
+ _path;
15
+ _enabled;
16
+ _sessionConfig;
17
+
18
+ constructor(config, { apiRouter, log, sessionConfig }) {
19
+ this._apiRouter = apiRouter;
20
+ this._log = log.extend({ logger: 'ws' });
21
+ this._pingIntervalMs = config?.pingIntervalMs ?? 3e4;
22
+ this._path = config?.path ?? '/ws';
23
+ this._sessionConfig = sessionConfig ?? null;
24
+
25
+ const wsRoutes = apiRouter.routes.filter((r) => r.wsEnabled);
26
+ this._enabled = wsRoutes.length > 0;
27
+
28
+ if (this._enabled) {
29
+ log.info(`WebSocket real-time: ${wsRoutes.length} route(s) on ${this._path}`);
30
+ }
31
+ }
32
+
33
+ attachToServer(httpServer) {
34
+ if (!this._enabled) return;
35
+
36
+ this._io = new SocketIOServer(httpServer, {
37
+ path: this._path,
38
+ serveClient: false,
39
+ transports: ['websocket'],
40
+ pingInterval: this._pingIntervalMs,
41
+ pingTimeout: this._pingIntervalMs * 2,
42
+ });
43
+
44
+ this._io.use(async (socket, next) => {
45
+ try {
46
+ const cookies = parseCookies({ headers: socket.handshake.headers });
47
+ const session = this._sessionConfig
48
+ ? await resolveSession(cookies, this._sessionConfig)
49
+ : null;
50
+
51
+ socket.data.session = session;
52
+ socket.data.cookies = cookies;
53
+ socket.data.headers = flattenHeaders(socket.handshake.headers);
54
+ next();
55
+ } catch (err) {
56
+ next(err);
57
+ }
58
+ });
59
+
60
+ this._io.on('connection', (socket) => this._onConnection(socket));
61
+
62
+ registerWsServer({
63
+ sendToSocket: (socketId, path, response) => this._sendToSocket(socketId, path, response),
64
+ broadcastToPath: (path, response) => this._broadcastToPath(path, response),
65
+ });
66
+ }
67
+
68
+ // Kept as no-op so WebServer's close() detection still works
69
+ handleUpgrade() {}
70
+
71
+ // No HTTP requests handled
72
+ handle() {
73
+ return false;
74
+ }
75
+
76
+ close() {
77
+ if (!this._enabled || !this._io) return;
78
+
79
+ unregisterWsServer();
80
+
81
+ const cleanupPromises = [];
82
+ for (const client of this._clients.values()) {
83
+ cleanupPromises.push(this._cleanupClient(client));
84
+ }
85
+
86
+ Promise.allSettled(cleanupPromises).then(() => {
87
+ this._io.close();
88
+ });
89
+ }
90
+
91
+ // ── Connection lifecycle ──
92
+
93
+ _onConnection(socket) {
94
+ const socketId = randomUUID();
95
+ const client = {
96
+ socket,
97
+ socketId,
98
+ subscriptions: new Map(),
99
+ session: socket.data.session,
100
+ headers: socket.data.headers,
101
+ cookies: socket.data.cookies,
102
+ };
103
+ this._clients.set(socket.id, client);
104
+ this._clientsBySocketId.set(socketId, client);
105
+
106
+ this._send(client, { path: this._path, data: { socketId }, status: 200 });
107
+
108
+ socket.on('msg', (data) => this._onMessage(client, data));
109
+ socket.on('disconnect', () => {
110
+ this._cleanupClient(client).catch((err) => {
111
+ this._log.error('WS client cleanup error on disconnect', { error: String(err) });
112
+ });
113
+ });
114
+ socket.on('error', (err) => {
115
+ this._log.error('WebSocket connection error', { error: String(err), socketId });
116
+ });
117
+ }
118
+
119
+ async _onMessage(client, parsed) {
120
+ if (!parsed || typeof parsed !== 'object') {
121
+ this._send(client, {
122
+ path: '',
123
+ error: { code: 'INVALID_MESSAGE', message: 'Invalid message' },
124
+ });
125
+ return;
126
+ }
127
+
128
+ if (!parsed.path || !parsed.method) {
129
+ this._send(client, {
130
+ path: '',
131
+ error: { code: 'INVALID_MESSAGE', message: 'Message must have "path" and "method" fields' },
132
+ });
133
+ return;
134
+ }
135
+
136
+ const method = parsed.method.toUpperCase();
137
+ if (method === 'SUBSCRIBE') {
138
+ await this._handleSubscribe(client, { ...parsed, method });
139
+ } else if (method === 'UNSUBSCRIBE') {
140
+ await this._handleUnsubscribe(client, { ...parsed, method });
141
+ } else if (['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
142
+ await this._handleMethodCall(client, { ...parsed, method });
143
+ } else {
144
+ this._send(client, {
145
+ path: parsed.path,
146
+ error: { code: 'INVALID_METHOD', message: `Unsupported method: ${method}` },
147
+ });
148
+ }
149
+ }
150
+
151
+ // ── Subscribe / Unsubscribe ──
152
+
153
+ async _handleSubscribe(client, wsMsg) {
154
+ const msgPath = wsMsg.path;
155
+ if (client.subscriptions.has(msgPath)) {
156
+ this._send(client, {
157
+ path: msgPath,
158
+ error: { code: 'ALREADY_SUBSCRIBED', message: 'Already subscribed to this path' },
159
+ });
160
+ return;
161
+ }
162
+
163
+ const matched = this._apiRouter.findRoute('GET', msgPath);
164
+ if (!matched) {
165
+ this._send(client, {
166
+ path: msgPath,
167
+ error: { code: 'NOT_FOUND', message: `No GET route matches ${msgPath}` },
168
+ status: 404,
169
+ });
170
+ return;
171
+ }
172
+
173
+ const { route, params } = matched;
174
+ if (!route.wsEnabled || typeof route.config.ws !== 'function') {
175
+ this._send(client, {
176
+ path: msgPath,
177
+ error: {
178
+ code: 'WS_NOT_SUPPORTED',
179
+ message: 'This route does not support WebSocket subscriptions',
180
+ },
181
+ status: 400,
182
+ });
183
+ return;
184
+ }
185
+
186
+ const reqInfo = this._buildRequestInfo(client, { ...wsMsg, method: 'GET' }, params);
187
+ let cleanup;
188
+ try {
189
+ const ctx = this._apiRouter.buildCtx(reqInfo);
190
+ cleanup = await route.config.ws(ctx);
191
+ } catch (err) {
192
+ this._log.error(`WS subscribe error for ${msgPath}`, {
193
+ error: toErrorMessage(err),
194
+ });
195
+ this._send(client, {
196
+ path: msgPath,
197
+ error: { code: 'SUBSCRIBE_ERROR', message: 'Subscription setup failed' },
198
+ status: 500,
199
+ });
200
+ return;
201
+ }
202
+
203
+ client.subscriptions.set(msgPath, {
204
+ routePattern: route.pattern,
205
+ cleanup: cleanup ?? undefined,
206
+ req: reqInfo,
207
+ });
208
+
209
+ const response = await this._apiRouter.executeRoute(route, reqInfo);
210
+ this._send(client, { path: msgPath, ...response, id: wsMsg.id });
211
+ }
212
+
213
+ async _handleUnsubscribe(client, wsMsg) {
214
+ const msgPath = wsMsg.path;
215
+ const sub = client.subscriptions.get(msgPath);
216
+
217
+ if (!sub) {
218
+ this._send(client, {
219
+ path: msgPath,
220
+ error: { code: 'NOT_SUBSCRIBED', message: 'Not subscribed to this path' },
221
+ });
222
+ return;
223
+ }
224
+
225
+ if (sub.cleanup) {
226
+ try {
227
+ await sub.cleanup();
228
+ } catch (err) {
229
+ this._log.error(`WS unsubscribe cleanup error for ${msgPath}`, {
230
+ error: toErrorMessage(err),
231
+ });
232
+ }
233
+ }
234
+
235
+ client.subscriptions.delete(msgPath);
236
+ }
237
+
238
+ // ── Method calls (GET/POST/PUT/PATCH/DELETE) ──
239
+
240
+ async _handleMethodCall(client, wsMsg) {
241
+ const { path: msgPath, method } = wsMsg;
242
+ const matched = this._apiRouter.findRoute(method, msgPath);
243
+
244
+ if (!matched) {
245
+ this._send(client, {
246
+ path: msgPath,
247
+ error: { code: 'NOT_FOUND', message: `No ${method} route matches ${msgPath}` },
248
+ status: 404,
249
+ id: wsMsg.id,
250
+ });
251
+ return;
252
+ }
253
+
254
+ const { route, params } = matched;
255
+ const reqInfo = this._buildRequestInfo(client, wsMsg, params);
256
+ const response = await this._apiRouter.executeRoute(route, reqInfo);
257
+ this._send(client, { path: msgPath, ...response, id: wsMsg.id });
258
+
259
+ if (!response.error) {
260
+ const msg = { path: msgPath, data: response.data, status: response.status };
261
+ for (const c of this._clients.values()) {
262
+ if (c !== client && c.subscriptions.has(msgPath)) {
263
+ this._send(c, msg);
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ // ── Internals ──
270
+
271
+ _buildRequestInfo(client, wsMsg, params) {
272
+ return {
273
+ requestId: randomUUID(),
274
+ method: wsMsg.method,
275
+ path: wsMsg.path,
276
+ query: { ...(wsMsg.query ?? {}), ...params },
277
+ body: wsMsg.body,
278
+ headers: client.headers,
279
+ cookies: client.cookies,
280
+ session: client.session,
281
+ socketId: client.socketId,
282
+ };
283
+ }
284
+
285
+ _send(client, msg) {
286
+ if (client.socket.connected) {
287
+ client.socket.emit('msg', msg);
288
+ }
289
+ }
290
+
291
+ _sendToSocket(socketId, path, response) {
292
+ const client = this._clientsBySocketId.get(socketId);
293
+ if (!client) return;
294
+ this._send(client, {
295
+ path,
296
+ data: response.data,
297
+ error: response.error,
298
+ status: response.status,
299
+ });
300
+ }
301
+
302
+ _broadcastToPath(path, response) {
303
+ const msg = {
304
+ path,
305
+ data: response.data,
306
+ error: response.error,
307
+ status: response.status,
308
+ };
309
+ for (const client of this._clients.values()) {
310
+ if (client.subscriptions.has(path)) {
311
+ this._send(client, msg);
312
+ }
313
+ }
314
+ }
315
+
316
+ async _cleanupClient(client) {
317
+ for (const [path, sub] of client.subscriptions) {
318
+ if (sub.cleanup) {
319
+ try {
320
+ await sub.cleanup();
321
+ } catch (err) {
322
+ this._log.error(`WS cleanup error for ${path}`, {
323
+ error: toErrorMessage(err),
324
+ });
325
+ }
326
+ }
327
+ }
328
+
329
+ client.subscriptions.clear();
330
+ this._clients.delete(client.socket.id);
331
+ this._clientsBySocketId.delete(client.socketId);
332
+ }
333
+ }
334
+
335
+ export { WsRouter };