dzql 0.5.33 → 0.6.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 (150) hide show
  1. package/.env.sample +28 -0
  2. package/compose.yml +28 -0
  3. package/dist/client/index.ts +1 -0
  4. package/dist/client/stores/useMyProfileStore.ts +114 -0
  5. package/dist/client/stores/useOrgDashboardStore.ts +131 -0
  6. package/dist/client/stores/useVenueDetailStore.ts +117 -0
  7. package/dist/client/ws.ts +716 -0
  8. package/dist/db/migrations/000_core.sql +92 -0
  9. package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
  10. package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
  11. package/dist/runtime/manifest.json +1562 -0
  12. package/docs/README.md +293 -36
  13. package/docs/feature-requests/applyPatch-bug-report.md +85 -0
  14. package/docs/feature-requests/connection-ready-profile.md +57 -0
  15. package/docs/feature-requests/hidden-bug-report.md +111 -0
  16. package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
  17. package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
  18. package/docs/feature-requests/todo.md +146 -0
  19. package/docs/for_ai.md +641 -0
  20. package/docs/project-setup.md +432 -0
  21. package/examples/blog.ts +50 -0
  22. package/examples/invalid.ts +18 -0
  23. package/examples/venues.js +485 -0
  24. package/package.json +23 -60
  25. package/src/cli/codegen/client.ts +99 -0
  26. package/src/cli/codegen/manifest.ts +95 -0
  27. package/src/cli/codegen/pinia.ts +174 -0
  28. package/src/cli/codegen/realtime.ts +58 -0
  29. package/src/cli/codegen/sql.ts +698 -0
  30. package/src/cli/codegen/subscribable_sql.ts +547 -0
  31. package/src/cli/codegen/subscribable_store.ts +184 -0
  32. package/src/cli/codegen/types.ts +142 -0
  33. package/src/cli/compiler/analyzer.ts +52 -0
  34. package/src/cli/compiler/graph_rules.ts +251 -0
  35. package/src/cli/compiler/ir.ts +233 -0
  36. package/src/cli/compiler/loader.ts +132 -0
  37. package/src/cli/compiler/permissions.ts +227 -0
  38. package/src/cli/index.ts +164 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/create/.env.example +8 -0
  42. package/src/create/README.md +101 -0
  43. package/src/create/compose.yml +14 -0
  44. package/src/create/domain.ts +153 -0
  45. package/src/create/package.json +24 -0
  46. package/src/create/server.ts +18 -0
  47. package/src/create/setup.sh +11 -0
  48. package/src/create/tsconfig.json +15 -0
  49. package/src/runtime/auth.ts +39 -0
  50. package/src/runtime/db.ts +33 -0
  51. package/src/runtime/errors.ts +51 -0
  52. package/src/runtime/index.ts +98 -0
  53. package/src/runtime/js_functions.ts +63 -0
  54. package/src/runtime/manifest_loader.ts +29 -0
  55. package/src/runtime/namespace.ts +483 -0
  56. package/src/runtime/server.ts +87 -0
  57. package/src/runtime/ws.ts +197 -0
  58. package/src/shared/ir.ts +197 -0
  59. package/tests/client.test.ts +38 -0
  60. package/tests/codegen.test.ts +71 -0
  61. package/tests/compiler.test.ts +45 -0
  62. package/tests/graph_rules.test.ts +173 -0
  63. package/tests/integration/db.test.ts +174 -0
  64. package/tests/integration/e2e.test.ts +65 -0
  65. package/tests/integration/features.test.ts +922 -0
  66. package/tests/integration/full_stack.test.ts +262 -0
  67. package/tests/integration/setup.ts +45 -0
  68. package/tests/ir.test.ts +32 -0
  69. package/tests/namespace.test.ts +395 -0
  70. package/tests/permissions.test.ts +55 -0
  71. package/tests/pinia.test.ts +48 -0
  72. package/tests/realtime.test.ts +22 -0
  73. package/tests/runtime.test.ts +80 -0
  74. package/tests/subscribable_gen.test.ts +72 -0
  75. package/tests/subscribable_reactivity.test.ts +258 -0
  76. package/tests/venues_gen.test.ts +25 -0
  77. package/tsconfig.json +20 -0
  78. package/tsconfig.tsbuildinfo +1 -0
  79. package/README.md +0 -90
  80. package/bin/cli.js +0 -727
  81. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  82. package/docs/compiler/CODING_STANDARDS.md +0 -415
  83. package/docs/compiler/COMPARISON.md +0 -673
  84. package/docs/compiler/QUICKSTART.md +0 -326
  85. package/docs/compiler/README.md +0 -134
  86. package/docs/examples/README.md +0 -38
  87. package/docs/examples/blog.sql +0 -160
  88. package/docs/examples/venue-detail-simple.sql +0 -8
  89. package/docs/examples/venue-detail-subscribable.sql +0 -45
  90. package/docs/for-ai/claude-guide.md +0 -1210
  91. package/docs/getting-started/quickstart.md +0 -125
  92. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  93. package/docs/getting-started/tutorial.md +0 -1104
  94. package/docs/guides/atomic-updates.md +0 -299
  95. package/docs/guides/client-stores.md +0 -730
  96. package/docs/guides/composite-primary-keys.md +0 -158
  97. package/docs/guides/custom-functions.md +0 -362
  98. package/docs/guides/drop-semantics.md +0 -554
  99. package/docs/guides/field-defaults.md +0 -240
  100. package/docs/guides/interpreter-vs-compiler.md +0 -237
  101. package/docs/guides/many-to-many.md +0 -929
  102. package/docs/guides/subscriptions.md +0 -537
  103. package/docs/reference/api.md +0 -1373
  104. package/docs/reference/client.md +0 -224
  105. package/src/client/stores/index.js +0 -8
  106. package/src/client/stores/useAppStore.js +0 -285
  107. package/src/client/stores/useWsStore.js +0 -289
  108. package/src/client/ws.js +0 -762
  109. package/src/compiler/cli/compile-example.js +0 -33
  110. package/src/compiler/cli/compile-subscribable.js +0 -43
  111. package/src/compiler/cli/debug-compile.js +0 -44
  112. package/src/compiler/cli/debug-parse.js +0 -26
  113. package/src/compiler/cli/debug-path-parser.js +0 -18
  114. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  115. package/src/compiler/cli/index.js +0 -174
  116. package/src/compiler/codegen/auth-codegen.js +0 -153
  117. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  118. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  119. package/src/compiler/codegen/notification-codegen.js +0 -232
  120. package/src/compiler/codegen/operation-codegen.js +0 -1382
  121. package/src/compiler/codegen/permission-codegen.js +0 -318
  122. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  123. package/src/compiler/compiler.js +0 -371
  124. package/src/compiler/index.js +0 -11
  125. package/src/compiler/parser/entity-parser.js +0 -440
  126. package/src/compiler/parser/path-parser.js +0 -290
  127. package/src/compiler/parser/subscribable-parser.js +0 -244
  128. package/src/database/dzql-core.sql +0 -161
  129. package/src/database/migrations/001_schema.sql +0 -60
  130. package/src/database/migrations/002_functions.sql +0 -890
  131. package/src/database/migrations/003_operations.sql +0 -1135
  132. package/src/database/migrations/004_search.sql +0 -581
  133. package/src/database/migrations/005_entities.sql +0 -730
  134. package/src/database/migrations/006_auth.sql +0 -94
  135. package/src/database/migrations/007_events.sql +0 -133
  136. package/src/database/migrations/008_hello.sql +0 -18
  137. package/src/database/migrations/008a_meta.sql +0 -172
  138. package/src/database/migrations/009_subscriptions.sql +0 -240
  139. package/src/database/migrations/010_atomic_updates.sql +0 -157
  140. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  141. package/src/index.js +0 -40
  142. package/src/server/api.js +0 -9
  143. package/src/server/db.js +0 -442
  144. package/src/server/index.js +0 -317
  145. package/src/server/logger.js +0 -259
  146. package/src/server/mcp.js +0 -594
  147. package/src/server/meta-route.js +0 -251
  148. package/src/server/namespace.js +0 -292
  149. package/src/server/subscriptions.js +0 -351
  150. package/src/server/ws.js +0 -573
package/src/server/ws.js DELETED
@@ -1,573 +0,0 @@
1
- import { SignJWT, jwtVerify } from "jose";
2
- import {
3
- callAuthFunction,
4
- callUserFunction,
5
- getUserProfile,
6
- db,
7
- sql,
8
- } from "./db.js";
9
- import { wsLogger, authLogger } from "./logger.js";
10
- import {
11
- registerSubscription,
12
- unregisterSubscription,
13
- unregisterSubscriptionByParams,
14
- removeConnectionSubscriptions,
15
- getSubscribableMetadata,
16
- cacheSubscribableMetadata
17
- } from "./subscriptions.js";
18
-
19
- // Environment configuration
20
- const JWT_SECRET_STRING = process.env.JWT_SECRET;
21
- const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "7d";
22
-
23
- // WebSocket ping/pong configuration (important for Heroku and other platforms)
24
- // Heroku terminates WebSocket connections after 55 seconds of inactivity (H15 error)
25
- // Default 30s interval keeps connections alive well within that limit
26
- const WS_PING_INTERVAL = parseInt(process.env.WS_PING_INTERVAL || "30000", 10); // 30 seconds default
27
- const WS_PING_TIMEOUT = parseInt(process.env.WS_PING_TIMEOUT || "5000", 10); // 5 seconds default
28
- const WS_MAX_MESSAGE_SIZE = parseInt(process.env.WS_MAX_MESSAGE_SIZE || "1048576", 10); // 1MB default
29
-
30
- // Validate JWT_SECRET in production
31
- if (process.env.NODE_ENV === "production" && !JWT_SECRET_STRING) {
32
- throw new Error(
33
- "JWT_SECRET environment variable is required in production. Generate one with: openssl rand -base64 32"
34
- );
35
- }
36
-
37
- // Warn if using default secret in development
38
- if (!JWT_SECRET_STRING && process.env.NODE_ENV !== "test") {
39
- console.warn(
40
- "⚠️ WARNING: Using default JWT secret. Set JWT_SECRET environment variable for security."
41
- );
42
- }
43
-
44
- const JWT_SECRET = new TextEncoder().encode(
45
- JWT_SECRET_STRING || "dev-secret-at-least-32-chars-long!!"
46
- );
47
-
48
- // JWT helpers
49
- export async function create_jwt(payload) {
50
- return await new SignJWT(payload)
51
- .setProtectedHeader({ alg: "HS256" })
52
- .setIssuedAt()
53
- .setExpirationTime(JWT_EXPIRES_IN)
54
- .sign(JWT_SECRET);
55
- }
56
-
57
- export async function verify_jwt_token(token) {
58
- try {
59
- const { payload } = await jwtVerify(token, JWT_SECRET);
60
- return payload;
61
- } catch (error) {
62
- return null;
63
- }
64
- }
65
-
66
- // JSON-RPC helpers
67
- export function create_rpc_response(id, result) {
68
- return JSON.stringify({
69
- jsonrpc: "2.0",
70
- result,
71
- id,
72
- });
73
- }
74
-
75
- export function create_rpc_error(id, code, message, data = null) {
76
- return JSON.stringify({
77
- jsonrpc: "2.0",
78
- error: { code, message, data },
79
- id,
80
- });
81
- }
82
-
83
- // SID (Session ID) Promise Management for bidirectional client-server communication
84
- // Used for async operations where server requests data from client
85
- const sidPromises = new Map();
86
-
87
- /**
88
- * Create a promise with a unique SID that can be resolved/rejected later
89
- * @param {number} timeout - Timeout in milliseconds (default: 30000)
90
- * @returns {Object} - { sid, promise }
91
- */
92
- export function createSIDPromise(timeout = 30000) {
93
- const sid = crypto.randomUUID();
94
-
95
- const promise = new Promise((resolve, reject) => {
96
- // Store resolve/reject functions
97
- sidPromises.set(sid, { resolve, reject });
98
-
99
- // Set timeout
100
- const timer = setTimeout(() => {
101
- if (sidPromises.has(sid)) {
102
- sidPromises.delete(sid);
103
- reject(new Error(`SID request timeout after ${timeout}ms`));
104
- }
105
- }, timeout);
106
-
107
- // Store timer so it can be cleared on resolution
108
- const entry = sidPromises.get(sid);
109
- if (entry) {
110
- entry.timer = timer;
111
- }
112
- });
113
-
114
- return { sid, promise };
115
- }
116
-
117
- /**
118
- * Resolve a pending SID promise with a result
119
- * @param {string} sid - The session ID
120
- * @param {any} result - The result to resolve with
121
- * @returns {boolean} - True if SID was found and resolved
122
- */
123
- export function resolveSID(sid, result) {
124
- const entry = sidPromises.get(sid);
125
- if (!entry) {
126
- wsLogger.warn(`Attempted to resolve unknown SID: ${sid}`);
127
- return false;
128
- }
129
-
130
- clearTimeout(entry.timer);
131
- sidPromises.delete(sid);
132
- entry.resolve(result);
133
- wsLogger.debug(`SID resolved: ${sid}`);
134
- return true;
135
- }
136
-
137
- /**
138
- * Reject a pending SID promise with an error
139
- * @param {string} sid - The session ID
140
- * @param {Error|string} error - The error to reject with
141
- * @returns {boolean} - True if SID was found and rejected
142
- */
143
- export function rejectSID(sid, error) {
144
- const entry = sidPromises.get(sid);
145
- if (!entry) {
146
- wsLogger.warn(`Attempted to reject unknown SID: ${sid}`);
147
- return false;
148
- }
149
-
150
- clearTimeout(entry.timer);
151
- sidPromises.delete(sid);
152
- entry.reject(typeof error === 'string' ? new Error(error) : error);
153
- wsLogger.debug(`SID rejected: ${sid}`);
154
- return true;
155
- }
156
-
157
- // Create RPC handler function
158
- export function createRPCHandler(customHandlers = {}) {
159
- return async function handle_rpc(ws, message) {
160
- let id = null;
161
- let method = null;
162
- const startTime = Date.now();
163
-
164
- try {
165
- const parsed = JSON.parse(message);
166
- method = parsed.method;
167
- const params = parsed.params;
168
- id = parsed.id;
169
-
170
- // Log incoming request
171
- wsLogger.request(method, params);
172
-
173
- // Handle SID responses from client (special internal method)
174
- if (method === "_sid_response") {
175
- const { sid, result, error } = params || {};
176
- if (!sid) {
177
- return create_rpc_error(id, -32602, "Missing sid parameter");
178
- }
179
-
180
- if (error) {
181
- rejectSID(sid, error);
182
- } else {
183
- resolveSID(sid, result);
184
- }
185
-
186
- return create_rpc_response(id, { success: true });
187
- }
188
-
189
- // Validate method doesn't start with underscore (private)
190
- if (method.startsWith("_")) {
191
- wsLogger.warn(`Blocked private function call: ${method}`);
192
- return create_rpc_error(id, -32601, "Cannot call private functions");
193
- }
194
-
195
- // Handle DZQL operations (require auth, identifiable by signature)
196
- if (method.startsWith("dzql.")) {
197
- if (!ws.data.user_id) {
198
- return create_rpc_error(id, -32603, "Not authenticated");
199
- }
200
-
201
- const [, operation, entity] = method.split(".");
202
- if (!operation || !entity) {
203
- return create_rpc_error(
204
- id,
205
- -32602,
206
- "Invalid DZQL method format. Use: dzql.operation.entity",
207
- );
208
- }
209
-
210
- if (
211
- !["get", "save", "delete", "lookup", "search"].includes(operation)
212
- ) {
213
- return create_rpc_error(
214
- id,
215
- -32602,
216
- `Unknown DZQL operation: ${operation}`,
217
- );
218
- }
219
-
220
- wsLogger.debug(`DZQL: Calling ${operation}.${entity} with params:`, JSON.stringify(params));
221
- const result = await db.api[operation][entity](
222
- params || {},
223
- ws.data.user_id,
224
- );
225
- wsLogger.debug(`DZQL: ${operation}.${entity} returned successfully`);
226
- return create_rpc_response(id, result);
227
- }
228
-
229
- // Local API functions that don't require auth
230
- if (method === "login_user") {
231
- authLogger.debug(`Login attempt for: ${params.email}`);
232
- const data = await callAuthFunction(
233
- "login_user",
234
- params.email,
235
- params.password,
236
- params.options || null,
237
- );
238
-
239
- // On successful auth, set user_id on WebSocket connection
240
- if (data && data.user_id) {
241
- ws.data.user_id = data.user_id;
242
- authLogger.info(`User logged in: ${params.email} (id: ${data.user_id})`);
243
-
244
- // Create JWT token for client storage
245
- const token = await create_jwt({
246
- user_id: data.user_id,
247
- email: data.email,
248
- });
249
-
250
- // Get full profile
251
- const profile = await getUserProfile(data.user_id);
252
-
253
- const result = {
254
- user_id: data.user_id,
255
- email: data.email,
256
- token,
257
- profile,
258
- };
259
- wsLogger.response(method, result, Date.now() - startTime);
260
- return create_rpc_response(id, result);
261
- }
262
-
263
- authLogger.warn(`Login failed for: ${params.email}`);
264
- wsLogger.response(method, data, Date.now() - startTime);
265
- return create_rpc_response(id, data);
266
- }
267
-
268
- if (method === "register_user") {
269
- authLogger.debug(`Registration attempt for: ${params.email}`);
270
- const data = await callAuthFunction(
271
- "register_user",
272
- params.email,
273
- params.password,
274
- params.options || null,
275
- );
276
-
277
- // On successful registration, set user_id on WebSocket connection
278
- if (data && data.user_id) {
279
- ws.data.user_id = data.user_id;
280
- authLogger.info(`User registered: ${params.email} (id: ${data.user_id})`);
281
-
282
- // Create JWT token for client storage
283
- const token = await create_jwt({
284
- user_id: data.user_id,
285
- email: data.email,
286
- });
287
-
288
- const result = {
289
- user_id: data.user_id,
290
- email: data.email,
291
- token,
292
- profile: data,
293
- };
294
- wsLogger.response(method, result, Date.now() - startTime);
295
- return create_rpc_response(id, result);
296
- }
297
-
298
- authLogger.warn(`Registration failed for: ${params.email}`);
299
- wsLogger.response(method, data, Date.now() - startTime);
300
- return create_rpc_response(id, data);
301
- }
302
-
303
- // Everything else requires authentication
304
- if (!ws.data.user_id) {
305
- wsLogger.warn(`Unauthenticated request to: ${method}`);
306
- return create_rpc_error(id, -32603, "Not authenticated");
307
- }
308
-
309
- // Authenticated-only local functions
310
- if (method === "logout") {
311
- authLogger.info(`User logged out (id: ${ws.data.user_id})`);
312
- ws.data.user_id = null;
313
- const result = { success: true };
314
- wsLogger.response(method, result, Date.now() - startTime);
315
- return create_rpc_response(id, result);
316
- }
317
-
318
- // SUBSCRIPTION HANDLERS - Pattern match on method name
319
- if (method.startsWith("subscribe_")) {
320
- const subscribableName = method.replace("subscribe_", "");
321
- wsLogger.debug(`Subscribe request: ${subscribableName}`, params);
322
-
323
- try {
324
- // Execute initial query (this also checks permissions)
325
- const queryResult = await sql.unsafe(
326
- `SELECT get_${subscribableName}($1, $2) as result`,
327
- [params, ws.data.user_id]
328
- );
329
-
330
- const queryData = queryResult[0]?.result;
331
-
332
- // Check if compiled function returned embedded schema
333
- // Compiled: { data, schema } | Interpreted: just the data object
334
- let data, schema;
335
- if (queryData && queryData.schema && queryData.data !== undefined) {
336
- // Compiled mode - schema is embedded (includes scopeTables)
337
- data = queryData.data;
338
- schema = queryData.schema;
339
- // Cache scopeTables for event filtering
340
- if (schema.scopeTables) {
341
- cacheSubscribableMetadata(subscribableName, {
342
- scopeTables: schema.scopeTables,
343
- pathMapping: schema.paths,
344
- rootEntity: schema.root,
345
- relations: {}
346
- });
347
- }
348
- } else {
349
- // Interpreted mode - fetch schema from metadata table
350
- data = queryData;
351
- const metadata = await getSubscribableMetadata(subscribableName, sql);
352
- schema = {
353
- root: metadata.rootEntity,
354
- paths: metadata.pathMapping
355
- };
356
- }
357
-
358
- // Register subscription in memory
359
- const subscriptionId = registerSubscription(
360
- subscribableName,
361
- ws.data.user_id,
362
- ws.data.connection_id,
363
- params
364
- );
365
-
366
- // Build result with schema for client-side patching
367
- const result = {
368
- subscription_id: subscriptionId,
369
- data,
370
- schema
371
- };
372
-
373
- wsLogger.response(method, result, Date.now() - startTime);
374
- return create_rpc_response(id, result);
375
- } catch (error) {
376
- wsLogger.error(`Subscribe failed for ${subscribableName}:`, error.message);
377
- return create_rpc_error(id, -32603, error.message);
378
- }
379
- }
380
-
381
- if (method.startsWith("unsubscribe_")) {
382
- const subscribableName = method.replace("unsubscribe_", "");
383
- wsLogger.debug(`Unsubscribe request: ${subscribableName}`, params);
384
-
385
- // Remove subscription by params
386
- const removed = unregisterSubscriptionByParams(
387
- subscribableName,
388
- ws.data.connection_id,
389
- params
390
- );
391
-
392
- const result = { success: removed };
393
- wsLogger.response(method, result, Date.now() - startTime);
394
- return create_rpc_response(id, result);
395
- }
396
-
397
- // Check for custom handlers
398
- if (customHandlers[method]) {
399
- wsLogger.debug(`Calling custom handler: ${method}`);
400
- const result = await customHandlers[method](ws.data.user_id, params);
401
- wsLogger.response(method, result, Date.now() - startTime);
402
- return create_rpc_response(id, result);
403
- }
404
-
405
- // Call stored function with user_id as first parameter
406
- wsLogger.debug(`Calling database function: ${method}`);
407
- const result = await callUserFunction(method, ws.data.user_id, params);
408
- wsLogger.response(method, result, Date.now() - startTime);
409
- return create_rpc_response(id, result);
410
- } catch (error) {
411
- wsLogger.error(`RPC error in ${method}:`, error.message);
412
- wsLogger.debug(`RPC error stack:`, error.stack);
413
-
414
- // PostgreSQL error codes
415
- if (error.code) {
416
- wsLogger.debug(`Returning PostgreSQL error for id=${id}`);
417
- return create_rpc_error(id, -32603, String(error), {
418
- code: error.code,
419
- });
420
- }
421
-
422
- // Generic error
423
- wsLogger.debug(`Returning generic error for id=${id}: ${error.message}`);
424
- return create_rpc_error(id, -32603, "Internal error", {
425
- message: error.message,
426
- });
427
- }
428
- };
429
- }
430
-
431
- // Create WebSocket event handlers
432
- export function createWebSocketHandlers(options = {}) {
433
- const {
434
- rpcHandler = null,
435
- customHandlers = {},
436
- onConnection = null,
437
- onDisconnection = null,
438
- } = options;
439
-
440
- // Active WebSocket connections
441
- const connections = new Map();
442
-
443
- // Create RPC handler if not provided
444
- const handler = rpcHandler || createRPCHandler(customHandlers);
445
-
446
- // Create broadcaster function
447
- const broadcast = createBroadcaster(connections);
448
-
449
- return {
450
- connections,
451
- broadcast,
452
-
453
- // WebSocket configuration for Bun.serve
454
- // These properties are required for proper ping/pong support (especially on Heroku)
455
- perMessageDeflate: true,
456
- maxPayloadLength: WS_MAX_MESSAGE_SIZE,
457
- idleTimeout: WS_PING_INTERVAL / 1000, // Convert to seconds for Bun
458
- closeOnBackpressureLimit: true, // Close connection if backpressure limit exceeded
459
-
460
- // Connection opened
461
- async open(ws) {
462
- const id = crypto.randomUUID();
463
- ws.data.connection_id = id;
464
- connections.set(id, ws);
465
-
466
- wsLogger.info(
467
- `Connection opened: ${id.slice(0, 8)}...`,
468
- ws.data.user_id ? `(user: ${ws.data.user_id})` : "(anonymous)",
469
- );
470
-
471
- // Get full profile if authenticated
472
- let profile = null;
473
- if (ws.data.user_id) {
474
- try {
475
- profile = await getUserProfile(ws.data.user_id);
476
- } catch (error) {
477
- wsLogger.error("Failed to load profile:", error.message);
478
- }
479
- }
480
-
481
- // Send welcome message as JSON-RPC method call
482
- ws.send(
483
- JSON.stringify({
484
- jsonrpc: "2.0",
485
- method: "connected",
486
- params: {
487
- connection_id: id,
488
- authenticated: !!ws.data.user_id,
489
- profile,
490
- },
491
- }),
492
- );
493
-
494
- // Call custom connection handler
495
- if (onConnection) {
496
- onConnection(ws, id);
497
- }
498
- },
499
-
500
- // Message received
501
- async message(ws, message) {
502
- const response = await handler(ws, message);
503
- ws.send(response);
504
- },
505
-
506
- // Connection closed
507
- close(ws) {
508
- const id = ws.data.connection_id;
509
- connections.delete(id);
510
-
511
- // Clean up all subscriptions for this connection
512
- const removedCount = removeConnectionSubscriptions(id);
513
- if (removedCount > 0) {
514
- wsLogger.info(`Connection closed: ${id?.slice(0, 8)}... (${removedCount} subscriptions removed)`);
515
- } else {
516
- wsLogger.info(`Connection closed: ${id?.slice(0, 8)}...`);
517
- }
518
-
519
- // Call custom disconnection handler
520
- if (onDisconnection) {
521
- onDisconnection(ws, id);
522
- }
523
- },
524
-
525
- // Error occurred
526
- error(ws, error) {
527
- wsLogger.error(`WebSocket error for ${ws.data.connection_id?.slice(0, 8)}...:`, error.message);
528
- },
529
- };
530
- }
531
-
532
- // Broadcast message to all authenticated connections or specific client_ids
533
- export function createBroadcaster(connections) {
534
- const broadcastToConnections = function(message, client_ids = null) {
535
- if (client_ids && Array.isArray(client_ids)) {
536
- // Send to specific user_ids
537
- for (const [id, ws] of connections) {
538
- if (ws.data.user_id && client_ids.includes(ws.data.user_id)) {
539
- ws.send(message);
540
- }
541
- }
542
- } else {
543
- // Send to all authenticated connections
544
- for (const [id, ws] of connections) {
545
- if (ws.data.user_id) {
546
- ws.send(message);
547
- }
548
- }
549
- }
550
- };
551
-
552
- // Add helper function to send to a specific connection
553
- broadcastToConnections.toConnection = function(connectionId, message) {
554
- const ws = connections.get(connectionId);
555
- if (ws && ws.readyState === 1) { // 1 = OPEN
556
- ws.send(message);
557
- return true;
558
- }
559
- return false;
560
- };
561
-
562
- return broadcastToConnections;
563
- }
564
-
565
- // Legacy export for backward compatibility
566
- export function broadcastToConnections(connections, message) {
567
- // Send to all authenticated connections
568
- for (const [id, ws] of connections) {
569
- if (ws.data.user_id) {
570
- ws.send(message);
571
- }
572
- }
573
- }