@sylphx/lens-server 2.3.2 → 2.4.1

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.
@@ -0,0 +1,559 @@
1
+ /**
2
+ * @sylphx/lens-server - WebSocket Handler Tests
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
6
+ import { mutation, query } from "@sylphx/lens-core";
7
+ import { z } from "zod";
8
+ import type { WebSocketLike } from "../server/create.js";
9
+ import { createApp } from "../server/create.js";
10
+ import { createWSHandler } from "./ws.js";
11
+
12
+ // =============================================================================
13
+ // Mock WebSocket
14
+ // =============================================================================
15
+
16
+ interface MockWebSocket extends WebSocketLike {
17
+ sentMessages: unknown[];
18
+ closeCode?: number;
19
+ closeReason?: string;
20
+ closed: boolean;
21
+ }
22
+
23
+ function createMockWebSocket(): MockWebSocket {
24
+ const ws: MockWebSocket = {
25
+ sentMessages: [],
26
+ closed: false,
27
+ send(data: string): void {
28
+ ws.sentMessages.push(JSON.parse(data));
29
+ },
30
+ close(code?: number, reason?: string): void {
31
+ ws.closeCode = code;
32
+ ws.closeReason = reason;
33
+ ws.closed = true;
34
+ },
35
+ onmessage: null,
36
+ onclose: null,
37
+ };
38
+ return ws;
39
+ }
40
+
41
+ // Helper to simulate receiving a message
42
+ function simulateMessage(ws: MockWebSocket, message: unknown): void {
43
+ if (ws.onmessage) {
44
+ ws.onmessage({ data: JSON.stringify(message) } as MessageEvent);
45
+ }
46
+ }
47
+
48
+ // Helper to wait for async operations
49
+ function wait(ms = 10): Promise<void> {
50
+ return new Promise((resolve) => setTimeout(resolve, ms));
51
+ }
52
+
53
+ // =============================================================================
54
+ // Test Queries and Mutations
55
+ // =============================================================================
56
+
57
+ const getUser = query()
58
+ .input(z.object({ id: z.string() }))
59
+ .resolve(({ input }) => ({
60
+ id: input.id,
61
+ name: "Test User",
62
+ __typename: "User",
63
+ }));
64
+
65
+ const listUsers = query().resolve(() => [
66
+ { id: "1", name: "User 1", __typename: "User" },
67
+ { id: "2", name: "User 2", __typename: "User" },
68
+ ]);
69
+
70
+ const createUser = mutation()
71
+ .input(z.object({ name: z.string() }))
72
+ .resolve(({ input }) => ({
73
+ id: "new-id",
74
+ name: input.name,
75
+ __typename: "User",
76
+ }));
77
+
78
+ const slowQuery = query()
79
+ .input(z.object({ delay: z.number() }))
80
+ .resolve(async ({ input }) => {
81
+ await new Promise((r) => setTimeout(r, input.delay));
82
+ return { done: true };
83
+ });
84
+
85
+ // =============================================================================
86
+ // Tests
87
+ // =============================================================================
88
+
89
+ describe("createWSHandler", () => {
90
+ let app: ReturnType<typeof createApp>;
91
+ let wsHandler: ReturnType<typeof createWSHandler>;
92
+
93
+ beforeEach(() => {
94
+ app = createApp({
95
+ queries: { getUser, listUsers, slowQuery },
96
+ mutations: { createUser },
97
+ });
98
+ wsHandler = createWSHandler(app);
99
+ });
100
+
101
+ afterEach(async () => {
102
+ await wsHandler.close();
103
+ });
104
+
105
+ describe("connection handling", () => {
106
+ it("creates a WebSocket handler from app", () => {
107
+ expect(wsHandler).toBeDefined();
108
+ expect(typeof wsHandler.handleConnection).toBe("function");
109
+ expect(typeof wsHandler.close).toBe("function");
110
+ expect(wsHandler.handler).toBeDefined();
111
+ });
112
+
113
+ it("accepts new connections", async () => {
114
+ const ws = createMockWebSocket();
115
+ wsHandler.handleConnection(ws);
116
+ await wait();
117
+
118
+ expect(ws.closed).toBe(false);
119
+ expect(ws.onmessage).not.toBeNull();
120
+ expect(ws.onclose).not.toBeNull();
121
+ });
122
+
123
+ it("rejects connections when at capacity", async () => {
124
+ const lowCapacityHandler = createWSHandler(app, { maxConnections: 2 });
125
+
126
+ const ws1 = createMockWebSocket();
127
+ const ws2 = createMockWebSocket();
128
+ const ws3 = createMockWebSocket();
129
+
130
+ lowCapacityHandler.handleConnection(ws1);
131
+ await wait();
132
+ lowCapacityHandler.handleConnection(ws2);
133
+ await wait();
134
+ lowCapacityHandler.handleConnection(ws3);
135
+ await wait();
136
+
137
+ expect(ws1.closed).toBe(false);
138
+ expect(ws2.closed).toBe(false);
139
+ expect(ws3.closed).toBe(true);
140
+ expect(ws3.closeCode).toBe(1013);
141
+ expect(ws3.closeReason).toBe("Server at capacity");
142
+
143
+ await lowCapacityHandler.close();
144
+ });
145
+
146
+ it("cleans up on disconnect", async () => {
147
+ const ws = createMockWebSocket();
148
+ wsHandler.handleConnection(ws);
149
+ await wait();
150
+
151
+ // Simulate disconnect
152
+ if (ws.onclose) {
153
+ ws.onclose({} as CloseEvent);
154
+ }
155
+ await wait();
156
+
157
+ // Connection should be cleaned up (no error on close)
158
+ await wsHandler.close();
159
+ });
160
+ });
161
+
162
+ describe("handshake", () => {
163
+ it("responds to handshake with metadata", async () => {
164
+ const ws = createMockWebSocket();
165
+ wsHandler.handleConnection(ws);
166
+ await wait();
167
+
168
+ simulateMessage(ws, {
169
+ type: "handshake",
170
+ id: "hs-1",
171
+ });
172
+ await wait();
173
+
174
+ expect(ws.sentMessages.length).toBe(1);
175
+ const response = ws.sentMessages[0] as {
176
+ type: string;
177
+ id: string;
178
+ version: string;
179
+ operations: Record<string, unknown>;
180
+ };
181
+ expect(response.type).toBe("handshake");
182
+ expect(response.id).toBe("hs-1");
183
+ expect(response.version).toBeDefined();
184
+ expect(response.operations).toBeDefined();
185
+ expect(response.operations.getUser).toBeDefined();
186
+ expect(response.operations.createUser).toBeDefined();
187
+ });
188
+ });
189
+
190
+ describe("query", () => {
191
+ it("executes query and returns result", async () => {
192
+ const ws = createMockWebSocket();
193
+ wsHandler.handleConnection(ws);
194
+ await wait();
195
+
196
+ simulateMessage(ws, {
197
+ type: "query",
198
+ id: "q-1",
199
+ operation: "getUser",
200
+ input: { id: "123" },
201
+ });
202
+ await wait();
203
+
204
+ expect(ws.sentMessages.length).toBe(1);
205
+ const response = ws.sentMessages[0] as {
206
+ type: string;
207
+ id: string;
208
+ data: { id: string; name: string };
209
+ };
210
+ expect(response.type).toBe("result");
211
+ expect(response.id).toBe("q-1");
212
+ expect(response.data.id).toBe("123");
213
+ expect(response.data.name).toBe("Test User");
214
+ });
215
+
216
+ it("applies field selection to query result", async () => {
217
+ const ws = createMockWebSocket();
218
+ wsHandler.handleConnection(ws);
219
+ await wait();
220
+
221
+ simulateMessage(ws, {
222
+ type: "query",
223
+ id: "q-1",
224
+ operation: "getUser",
225
+ input: { id: "123" },
226
+ fields: ["name"], // Only request name field
227
+ });
228
+ await wait();
229
+
230
+ const response = ws.sentMessages[0] as {
231
+ type: string;
232
+ data: { id: string; name: string };
233
+ };
234
+ expect(response.data.id).toBe("123"); // id always included
235
+ expect(response.data.name).toBe("Test User");
236
+ expect((response.data as { __typename?: string }).__typename).toBeUndefined();
237
+ });
238
+
239
+ it("returns error for unknown operation", async () => {
240
+ const ws = createMockWebSocket();
241
+ wsHandler.handleConnection(ws);
242
+ await wait();
243
+
244
+ simulateMessage(ws, {
245
+ type: "query",
246
+ id: "q-1",
247
+ operation: "unknownOp",
248
+ input: {},
249
+ });
250
+ await wait();
251
+
252
+ const response = ws.sentMessages[0] as {
253
+ type: string;
254
+ id: string;
255
+ error: { code: string; message: string };
256
+ };
257
+ expect(response.type).toBe("error");
258
+ expect(response.id).toBe("q-1");
259
+ expect(response.error.code).toBe("EXECUTION_ERROR");
260
+ });
261
+ });
262
+
263
+ describe("mutation", () => {
264
+ it("executes mutation and returns result", async () => {
265
+ const ws = createMockWebSocket();
266
+ wsHandler.handleConnection(ws);
267
+ await wait();
268
+
269
+ simulateMessage(ws, {
270
+ type: "mutation",
271
+ id: "m-1",
272
+ operation: "createUser",
273
+ input: { name: "New User" },
274
+ });
275
+ await wait();
276
+
277
+ const response = ws.sentMessages[0] as {
278
+ type: string;
279
+ id: string;
280
+ data: { id: string; name: string };
281
+ };
282
+ expect(response.type).toBe("result");
283
+ expect(response.id).toBe("m-1");
284
+ expect(response.data.id).toBe("new-id");
285
+ expect(response.data.name).toBe("New User");
286
+ });
287
+ });
288
+
289
+ describe("subscribe/unsubscribe", () => {
290
+ it("creates subscription without error", async () => {
291
+ const ws = createMockWebSocket();
292
+ wsHandler.handleConnection(ws);
293
+ await wait();
294
+
295
+ simulateMessage(ws, {
296
+ type: "subscribe",
297
+ id: "sub-1",
298
+ operation: "getUser",
299
+ input: { id: "123" },
300
+ fields: "*",
301
+ });
302
+ await wait(50); // Give time for async subscription setup
303
+
304
+ // Subscription should be established without errors
305
+ // (No error message with id "sub-1")
306
+ const errorMsg = ws.sentMessages.find(
307
+ (msg) => (msg as { type?: string; id?: string }).type === "error" && (msg as { id?: string }).id === "sub-1",
308
+ );
309
+ expect(errorMsg).toBeUndefined();
310
+ expect(ws.closed).toBe(false);
311
+ });
312
+
313
+ it("handles unsubscribe", async () => {
314
+ const ws = createMockWebSocket();
315
+ wsHandler.handleConnection(ws);
316
+ await wait();
317
+
318
+ // Subscribe first
319
+ simulateMessage(ws, {
320
+ type: "subscribe",
321
+ id: "sub-1",
322
+ operation: "getUser",
323
+ input: { id: "123" },
324
+ fields: "*",
325
+ });
326
+ await wait(50);
327
+
328
+ const _messagesBeforeUnsub = ws.sentMessages.length;
329
+
330
+ // Unsubscribe
331
+ simulateMessage(ws, {
332
+ type: "unsubscribe",
333
+ id: "sub-1",
334
+ });
335
+ await wait();
336
+
337
+ // Should not receive any more messages (or just an ack)
338
+ // Main thing is no error
339
+ expect(ws.closed).toBe(false);
340
+ });
341
+
342
+ it("enforces subscription limit per client", async () => {
343
+ const limitedHandler = createWSHandler(app, { maxSubscriptionsPerClient: 2 });
344
+
345
+ const ws = createMockWebSocket();
346
+ limitedHandler.handleConnection(ws);
347
+ await wait();
348
+
349
+ // Create subscriptions up to limit
350
+ simulateMessage(ws, {
351
+ type: "subscribe",
352
+ id: "sub-1",
353
+ operation: "getUser",
354
+ input: { id: "1" },
355
+ fields: "*",
356
+ });
357
+ await wait(50);
358
+
359
+ simulateMessage(ws, {
360
+ type: "subscribe",
361
+ id: "sub-2",
362
+ operation: "getUser",
363
+ input: { id: "2" },
364
+ fields: "*",
365
+ });
366
+ await wait(50);
367
+
368
+ // Try to exceed limit
369
+ simulateMessage(ws, {
370
+ type: "subscribe",
371
+ id: "sub-3",
372
+ operation: "getUser",
373
+ input: { id: "3" },
374
+ fields: "*",
375
+ });
376
+ await wait(50);
377
+
378
+ // Find the error message for sub-3
379
+ const errorMsg = ws.sentMessages.find(
380
+ (msg) => (msg as { type?: string; id?: string }).type === "error" && (msg as { id?: string }).id === "sub-3",
381
+ ) as { error?: { code?: string } } | undefined;
382
+
383
+ expect(errorMsg).toBeDefined();
384
+ expect(errorMsg?.error?.code).toBe("SUBSCRIPTION_LIMIT");
385
+
386
+ await limitedHandler.close();
387
+ });
388
+ });
389
+
390
+ describe("message size limits", () => {
391
+ it("rejects messages exceeding size limit", async () => {
392
+ const limitedHandler = createWSHandler(app, { maxMessageSize: 100 });
393
+
394
+ const ws = createMockWebSocket();
395
+ limitedHandler.handleConnection(ws);
396
+ await wait();
397
+
398
+ // Create a large message
399
+ const largeMessage = JSON.stringify({
400
+ type: "query",
401
+ id: "q-1",
402
+ operation: "getUser",
403
+ input: { id: "x".repeat(200) },
404
+ });
405
+
406
+ // Simulate receiving large message directly
407
+ if (ws.onmessage) {
408
+ ws.onmessage({ data: largeMessage } as MessageEvent);
409
+ }
410
+ await wait();
411
+
412
+ const errorMsg = ws.sentMessages.find((msg) => (msg as { type?: string }).type === "error") as
413
+ | { error?: { code?: string } }
414
+ | undefined;
415
+
416
+ expect(errorMsg).toBeDefined();
417
+ expect(errorMsg?.error?.code).toBe("MESSAGE_TOO_LARGE");
418
+
419
+ await limitedHandler.close();
420
+ });
421
+ });
422
+
423
+ describe("rate limiting", () => {
424
+ it("enforces rate limit on messages", async () => {
425
+ const limitedHandler = createWSHandler(app, {
426
+ rateLimit: { maxMessages: 3, windowMs: 1000 },
427
+ });
428
+
429
+ const ws = createMockWebSocket();
430
+ limitedHandler.handleConnection(ws);
431
+ await wait();
432
+
433
+ // Send messages rapidly
434
+ for (let i = 0; i < 5; i++) {
435
+ simulateMessage(ws, {
436
+ type: "query",
437
+ id: `q-${i}`,
438
+ operation: "getUser",
439
+ input: { id: String(i) },
440
+ });
441
+ }
442
+ await wait();
443
+
444
+ // Should have some rate limited errors
445
+ const rateLimitErrors = ws.sentMessages.filter(
446
+ (msg) =>
447
+ (msg as { type?: string }).type === "error" &&
448
+ (msg as { error?: { code?: string } }).error?.code === "RATE_LIMITED",
449
+ );
450
+
451
+ expect(rateLimitErrors.length).toBeGreaterThan(0);
452
+
453
+ await limitedHandler.close();
454
+ });
455
+ });
456
+
457
+ describe("error handling", () => {
458
+ it("handles JSON parse errors", async () => {
459
+ const ws = createMockWebSocket();
460
+ wsHandler.handleConnection(ws);
461
+ await wait();
462
+
463
+ // Send invalid JSON directly
464
+ if (ws.onmessage) {
465
+ ws.onmessage({ data: "not valid json {" } as MessageEvent);
466
+ }
467
+ await wait();
468
+
469
+ const errorMsg = ws.sentMessages[0] as { type?: string; error?: { code?: string } };
470
+ expect(errorMsg.type).toBe("error");
471
+ expect(errorMsg.error?.code).toBe("PARSE_ERROR");
472
+ });
473
+
474
+ it("handles validation errors gracefully", async () => {
475
+ const ws = createMockWebSocket();
476
+ wsHandler.handleConnection(ws);
477
+ await wait();
478
+
479
+ // Send query with invalid input (missing required field)
480
+ simulateMessage(ws, {
481
+ type: "query",
482
+ id: "q-1",
483
+ operation: "getUser",
484
+ input: {}, // Missing required 'id' field
485
+ });
486
+ await wait();
487
+
488
+ const errorMsg = ws.sentMessages[0] as {
489
+ type?: string;
490
+ id?: string;
491
+ error?: { code?: string };
492
+ };
493
+ expect(errorMsg.type).toBe("error");
494
+ expect(errorMsg.id).toBe("q-1");
495
+ expect(errorMsg.error?.code).toBe("EXECUTION_ERROR");
496
+ });
497
+ });
498
+
499
+ describe("Bun handler interface", () => {
500
+ it("provides Bun-compatible handler object", () => {
501
+ expect(wsHandler.handler).toBeDefined();
502
+ expect(typeof wsHandler.handler.message).toBe("function");
503
+ expect(typeof wsHandler.handler.close).toBe("function");
504
+ expect(typeof wsHandler.handler.open).toBe("function");
505
+ });
506
+
507
+ it("handler.message routes to correct connection", async () => {
508
+ const ws = createMockWebSocket();
509
+
510
+ // First establish connection via open
511
+ wsHandler.handler.open?.(ws);
512
+ await wait();
513
+
514
+ // Send message via handler.message
515
+ const messageData = JSON.stringify({
516
+ type: "query",
517
+ id: "q-1",
518
+ operation: "getUser",
519
+ input: { id: "123" },
520
+ });
521
+
522
+ wsHandler.handler.message(ws, messageData);
523
+ await wait();
524
+
525
+ const response = ws.sentMessages[0] as { type?: string; id?: string };
526
+ expect(response.type).toBe("result");
527
+ expect(response.id).toBe("q-1");
528
+ });
529
+
530
+ it("handler.close triggers disconnect", async () => {
531
+ const ws = createMockWebSocket();
532
+ wsHandler.handleConnection(ws);
533
+ await wait();
534
+
535
+ // Close via handler
536
+ wsHandler.handler.close(ws);
537
+ await wait();
538
+
539
+ // Should have cleaned up without error
540
+ await wsHandler.close();
541
+ });
542
+ });
543
+
544
+ describe("close", () => {
545
+ it("closes all connections on handler close", async () => {
546
+ const ws1 = createMockWebSocket();
547
+ const ws2 = createMockWebSocket();
548
+
549
+ wsHandler.handleConnection(ws1);
550
+ wsHandler.handleConnection(ws2);
551
+ await wait();
552
+
553
+ await wsHandler.close();
554
+
555
+ expect(ws1.closed).toBe(true);
556
+ expect(ws2.closed).toBe(true);
557
+ });
558
+ });
559
+ });
@@ -81,6 +81,47 @@ export type { WSHandler, WSHandlerOptions } from "./ws-types.js";
81
81
  export function createWSHandler(server: LensServer, options: WSHandlerOptions = {}): WSHandler {
82
82
  const { logger = {} } = options;
83
83
 
84
+ // Security limits with sensible defaults
85
+ const maxMessageSize = options.maxMessageSize ?? 1024 * 1024; // 1MB
86
+ const maxSubscriptionsPerClient = options.maxSubscriptionsPerClient ?? 100;
87
+ const maxConnections = options.maxConnections ?? 10000;
88
+
89
+ // Rate limiting configuration
90
+ const rateLimitMaxMessages = options.rateLimit?.maxMessages ?? 100;
91
+ const rateLimitWindowMs = options.rateLimit?.windowMs ?? 1000;
92
+
93
+ // Rate limit tracking per client (sliding window)
94
+ const clientMessageTimestamps = new Map<string, number[]>();
95
+
96
+ /**
97
+ * Check if client is rate limited using sliding window algorithm.
98
+ * Returns true if the message should be rejected.
99
+ */
100
+ function isRateLimited(clientId: string): boolean {
101
+ const now = Date.now();
102
+ const windowStart = now - rateLimitWindowMs;
103
+
104
+ let timestamps = clientMessageTimestamps.get(clientId);
105
+ if (!timestamps) {
106
+ timestamps = [];
107
+ clientMessageTimestamps.set(clientId, timestamps);
108
+ }
109
+
110
+ // Remove expired timestamps (outside window)
111
+ while (timestamps.length > 0 && timestamps[0] < windowStart) {
112
+ timestamps.shift();
113
+ }
114
+
115
+ // Check if over limit
116
+ if (timestamps.length >= rateLimitMaxMessages) {
117
+ return true;
118
+ }
119
+
120
+ // Record this message
121
+ timestamps.push(now);
122
+ return false;
123
+ }
124
+
84
125
  // Connection tracking
85
126
  const connections = new Map<string, ClientConnection>();
86
127
  const wsToConnection = new WeakMap<object, ClientConnection>();
@@ -88,6 +129,13 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
88
129
 
89
130
  // Handle new WebSocket connection
90
131
  async function handleConnection(ws: WebSocketLike): Promise<void> {
132
+ // Check connection limit
133
+ if (connections.size >= maxConnections) {
134
+ logger.warn?.(`Connection limit reached (${maxConnections}), rejecting new connection`);
135
+ ws.close(1013, "Server at capacity");
136
+ return;
137
+ }
138
+
91
139
  const clientId = `client_${++connectionCounter}`;
92
140
 
93
141
  const conn: ClientConnection = {
@@ -122,6 +170,36 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
122
170
 
123
171
  // Handle incoming message
124
172
  function handleMessage(conn: ClientConnection, data: string): void {
173
+ // Check message size limit
174
+ if (data.length > maxMessageSize) {
175
+ logger.warn?.(`Message too large (${data.length} bytes > ${maxMessageSize}), rejecting`);
176
+ conn.ws.send(
177
+ JSON.stringify({
178
+ type: "error",
179
+ error: {
180
+ code: "MESSAGE_TOO_LARGE",
181
+ message: `Message exceeds ${maxMessageSize} byte limit`,
182
+ },
183
+ }),
184
+ );
185
+ return;
186
+ }
187
+
188
+ // Check rate limit
189
+ if (isRateLimited(conn.id)) {
190
+ logger.warn?.(`Rate limit exceeded for client ${conn.id}`);
191
+ conn.ws.send(
192
+ JSON.stringify({
193
+ type: "error",
194
+ error: {
195
+ code: "RATE_LIMITED",
196
+ message: `Rate limit exceeded: max ${rateLimitMaxMessages} messages per ${rateLimitWindowMs}ms`,
197
+ },
198
+ }),
199
+ );
200
+ return;
201
+ }
202
+
125
203
  try {
126
204
  const message = JSON.parse(data) as ClientMessage;
127
205
 
@@ -175,6 +253,24 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
175
253
  async function handleSubscribe(conn: ClientConnection, message: SubscribeMessage): Promise<void> {
176
254
  const { id, operation, input, fields } = message;
177
255
 
256
+ // Check subscription limit (skip if replacing existing subscription)
257
+ if (!conn.subscriptions.has(id) && conn.subscriptions.size >= maxSubscriptionsPerClient) {
258
+ logger.warn?.(
259
+ `Subscription limit reached for client ${conn.id} (${maxSubscriptionsPerClient}), rejecting`,
260
+ );
261
+ conn.ws.send(
262
+ JSON.stringify({
263
+ type: "error",
264
+ id,
265
+ error: {
266
+ code: "SUBSCRIPTION_LIMIT",
267
+ message: `Maximum ${maxSubscriptionsPerClient} subscriptions per client`,
268
+ },
269
+ }),
270
+ );
271
+ return;
272
+ }
273
+
178
274
  // Execute query first to get data
179
275
  let result: { data?: unknown; error?: Error };
180
276
  try {
@@ -550,6 +646,9 @@ export function createWSHandler(server: LensServer, options: WSHandlerOptions =
550
646
  // Remove connection
551
647
  connections.delete(conn.id);
552
648
 
649
+ // Cleanup rate limit tracking
650
+ clientMessageTimestamps.delete(conn.id);
651
+
553
652
  // Server handles removal (plugin hooks + state manager cleanup)
554
653
  server.removeClient(conn.id, subscriptionCount);
555
654
  }
package/src/index.ts CHANGED
@@ -186,3 +186,24 @@ export {
186
186
  // =============================================================================
187
187
 
188
188
  export { coalescePatches, estimatePatchSize, OperationLog } from "./reconnect/index.js";
189
+
190
+ // =============================================================================
191
+ // Logging
192
+ // =============================================================================
193
+
194
+ export {
195
+ createStructuredLogger,
196
+ type ErrorContext,
197
+ jsonOutput,
198
+ type LogContext,
199
+ type LogEntry,
200
+ type LogLevel,
201
+ type LogOutput,
202
+ type PerformanceContext,
203
+ prettyOutput,
204
+ type RequestContext,
205
+ type StructuredLogger,
206
+ type StructuredLoggerOptions,
207
+ toBasicLogger,
208
+ type WebSocketContext,
209
+ } from "./logging/index.js";