@leaven-graphql/ws 0.2.1 → 0.2.2

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.
package/README.md CHANGED
@@ -283,6 +283,11 @@ interface WebSocketHandlerConfig<TContext = unknown> {
283
283
  keepAliveInterval?: number;
284
284
  /** Forwarded to the SubscriptionManager (minus `schema`) */
285
285
  subscriptionManager?: Omit<SubscriptionManagerConfig, 'schema'>;
286
+ /**
287
+ * Destination for diagnostics such as failing hooks (default `console`).
288
+ * Passed on to the SubscriptionManager unless it sets its own.
289
+ */
290
+ logger?: WebSocketLogger;
286
291
  /** Return false to reject the connection with 4403 */
287
292
  onConnect?: (
288
293
  socket: ServerWebSocket<WebSocketContext>,
@@ -313,7 +318,22 @@ interface WebSocketContext {
313
318
  Close codes follow the protocol: `4400` invalid message, `4401` unauthorized
314
319
  (operation before `connection_init`), `4403` rejected by `onConnect`, `4408`
315
320
  init timeout, `4409` duplicate subscription id, `4429` repeated
316
- `connection_init`.
321
+ `connection_init` (including a second one sent while `onConnect` for the
322
+ first is still running).
323
+
324
+ An operation is tracked from the moment its `subscribe` frame arrives. A
325
+ client `complete` for an operation that is still setting up cancels it, and
326
+ an operation still setting up when the socket closes is abandoned: in both
327
+ cases nothing is registered and any source stream it obtained is returned.
328
+ Operations still setting up count toward `maxSubscriptionsPerConnection`.
329
+
330
+ ```typescript
331
+ interface WebSocketLogger {
332
+ error(...args: unknown[]): void;
333
+ /** Optional; warnings go to `error` without it */
334
+ warn?(...args: unknown[]): void;
335
+ }
336
+ ```
317
337
 
318
338
  ### SubscriptionManager
319
339
 
@@ -334,6 +354,9 @@ class SubscriptionManager {
334
354
  onError: (errors: readonly { message: string }[]) => void
335
355
  ): Promise<Subscription>;
336
356
 
357
+ /** Parse with the executor's limits (maxTokens, maxDepth) */
358
+ parse(query: string): Promise<DocumentNode>;
359
+
337
360
  /** Run a query or mutation on the same executor */
338
361
  execute<TContext = unknown>(
339
362
  request: GraphQLRequest,
@@ -348,6 +371,8 @@ class SubscriptionManager {
348
371
 
349
372
  readonly subscriptionCount: number;
350
373
  readonly connectionCount: number;
374
+ /** maxSubscriptionsPerConnection */
375
+ readonly subscriptionLimit: number;
351
376
  }
352
377
 
353
378
  function createSubscriptionManager(config: SubscriptionManagerConfig): SubscriptionManager;
@@ -365,6 +390,8 @@ interface SubscriptionManagerConfig {
365
390
  * `introspection: false`, `maxDepth` or `maxComplexity`.
366
391
  */
367
392
  executor?: LeavenExecutor | Omit<ExecutorConfig, 'schema'>;
393
+ /** Destination for timeout and callback diagnostics (default `console`) */
394
+ logger?: WebSocketLogger;
368
395
  }
369
396
 
370
397
  interface Subscription {
@@ -380,7 +407,9 @@ interface Subscription {
380
407
 
381
408
  Subscription ids are scoped to a connection: `graphql-ws` clients number their
382
409
  operations per connection (starting at `"1"`), so every lookup and teardown
383
- takes **both** the connection id and the subscription id.
410
+ takes **both** the connection id and the subscription id. `subscribe` throws
411
+ when the id is already registered on that connection rather than overwriting
412
+ it.
384
413
 
385
414
  ### PubSub
386
415
 
@@ -409,6 +438,8 @@ interface PubSubConfig {
409
438
  * events instead of an unbounded queue. Unlimited by default.
410
439
  */
411
440
  maxQueueSize?: number;
441
+ /** Destination for subscriber callback errors (default `console`) */
442
+ logger?: WebSocketLogger;
412
443
  }
413
444
  ```
414
445
 
package/dist/handler.d.ts CHANGED
@@ -8,6 +8,7 @@ import { type GraphQLSchema } from 'graphql';
8
8
  import type { ServerWebSocket } from 'bun';
9
9
  import type { GraphQLRequest } from '@leaven-graphql/core';
10
10
  import { type SubscriptionManagerConfig } from './manager';
11
+ import type { WebSocketLogger } from './logger';
11
12
  /**
12
13
  * WebSocket context data
13
14
  */
@@ -39,6 +40,12 @@ export interface WebSocketHandlerConfig<TContext = unknown> {
39
40
  keepAliveInterval?: number;
40
41
  /** Subscription manager config */
41
42
  subscriptionManager?: Omit<SubscriptionManagerConfig, 'schema'>;
43
+ /**
44
+ * Where diagnostics (failing hooks and callbacks) are logged. Defaults to
45
+ * `console`. Also passed to the subscription manager unless
46
+ * `subscriptionManager.logger` is set.
47
+ */
48
+ logger?: WebSocketLogger;
42
49
  /** Called when a connection is initialized */
43
50
  onConnect?: (socket: ServerWebSocket<WebSocketContext>, params?: Record<string, unknown>) => boolean | Promise<boolean>;
44
51
  /** Called when a connection is closed */
@@ -61,9 +68,19 @@ export declare class WebSocketHandler<TContext = unknown> {
61
68
  private readonly onDisconnect?;
62
69
  private readonly onSubscribe?;
63
70
  private readonly onComplete?;
71
+ private readonly logger;
64
72
  private readonly initTimeouts;
65
73
  private readonly keepAliveTimers;
74
+ private readonly states;
66
75
  constructor(config: WebSocketHandlerConfig<TContext>);
76
+ /**
77
+ * Lifecycle state for a socket, created on first use.
78
+ */
79
+ private stateOf;
80
+ /**
81
+ * Stop and forget a connection's keep-alive timer, if it has one.
82
+ */
83
+ private clearKeepAlive;
67
84
  /**
68
85
  * Generate a unique connection ID
69
86
  */
@@ -104,7 +121,11 @@ export declare class WebSocketHandler<TContext = unknown> {
104
121
  * A document graphql-js cannot resolve to a single operation (no operations
105
122
  * at all, or several with no `operationName`) is reported as a subscription
106
123
  * so the executor produces the precise error rather than this method
107
- * guessing at one.
124
+ * guessing at one. The same goes for a document that fails to parse.
125
+ *
126
+ * Parsing goes through the executor, so the configured `maxTokens` and
127
+ * depth limits apply here too; a plain `parse()` would lex an arbitrarily
128
+ * large document before the executor's limits ever ran.
108
129
  */
109
130
  private isSubscriptionOperation;
110
131
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export type { WebSocketHandlerConfig, WebSocketContext } from './handler';
9
9
  export { SubscriptionManager, createSubscriptionManager } from './manager';
10
10
  export type { SubscriptionManagerConfig, Subscription, SubscriptionStatus } from './manager';
11
11
  export { PubSub, createPubSub } from './pubsub';
12
+ export type { WebSocketLogger } from './logger';
12
13
  export type { PubSubConfig, PubSubEngine, SubscribeFn, PublishFn } from './pubsub';
13
14
  export { MessageType, parseMessage, formatMessage, createConnectionAck, createNextMessage, createErrorMessage, createCompleteMessage, createPongMessage, type ConnectionInitMessage, type ConnectionAckMessage, type SubscribeMessage, type NextMessage, type ErrorMessage, type CompleteMessage, type PingMessage, type PongMessage, type ParseMessageOptions, } from './protocol';
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACrE,YAAY,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE1E,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC;AAC3E,YAAY,EAAE,yBAAyB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE7F,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAChD,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEnF,OAAO,EACL,WAAW,EACX,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,EACjB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACrE,YAAY,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE1E,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC;AAC3E,YAAY,EAAE,yBAAyB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE7F,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAChD,YAAY,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAChD,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEnF,OAAO,EACL,WAAW,EACX,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,EACjB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,22 +1,40 @@
1
1
  // @bun
2
2
  // src/handler.ts
3
- import { OperationTypeNode, getOperationAST, parse } from "graphql";
3
+ import { OperationTypeNode, getOperationAST } from "graphql";
4
4
 
5
5
  // src/manager.ts
6
6
  import {
7
7
  LeavenExecutor
8
8
  } from "@leaven-graphql/core";
9
9
 
10
+ // src/logger.ts
11
+ function logWarning(logger, ...args) {
12
+ if (logger.warn) {
13
+ logger.warn(...args);
14
+ } else {
15
+ logger.error(...args);
16
+ }
17
+ }
18
+
19
+ // src/manager.ts
10
20
  class SubscriptionManager {
11
21
  executor;
12
22
  connections;
13
23
  maxSubscriptionsPerConnection;
14
24
  subscriptionTimeout;
25
+ logger;
15
26
  constructor(config) {
16
27
  this.executor = config.executor instanceof LeavenExecutor ? config.executor : new LeavenExecutor({ ...config.executor, schema: config.schema });
17
28
  this.connections = new Map;
18
29
  this.maxSubscriptionsPerConnection = config.maxSubscriptionsPerConnection ?? 100;
19
30
  this.subscriptionTimeout = config.subscriptionTimeout ?? 0;
31
+ this.logger = config.logger ?? console;
32
+ }
33
+ get subscriptionLimit() {
34
+ return this.maxSubscriptionsPerConnection;
35
+ }
36
+ parse(query) {
37
+ return this.executor.parse(query);
20
38
  }
21
39
  async execute(request, context) {
22
40
  const { response } = await this.executor.execute(request, context);
@@ -27,6 +45,9 @@ class SubscriptionManager {
27
45
  if (existing && existing.size >= this.maxSubscriptionsPerConnection) {
28
46
  throw new Error("Maximum subscriptions per connection reached");
29
47
  }
48
+ if (existing?.has(subscriptionId)) {
49
+ throw new Error(`Subscription ${subscriptionId} already exists`);
50
+ }
30
51
  const subscription = {
31
52
  id: subscriptionId,
32
53
  connectionId,
@@ -42,6 +63,13 @@ class SubscriptionManager {
42
63
  connectionSubs.set(subscriptionId, subscription);
43
64
  try {
44
65
  const result = await this.executor.subscribe(request, context);
66
+ if (!this.isCurrent(subscription)) {
67
+ subscription.status = "completed";
68
+ if (Symbol.asyncIterator in Object(result)) {
69
+ result.return?.()?.catch(() => {});
70
+ }
71
+ return subscription;
72
+ }
45
73
  if (!(Symbol.asyncIterator in Object(result))) {
46
74
  subscription.status = "error";
47
75
  const errors = result.errors ?? [{ message: "Subscription failed" }];
@@ -57,15 +85,17 @@ class SubscriptionManager {
57
85
  timeoutId = setTimeout(() => {
58
86
  try {
59
87
  const timedOut = this.getSubscription(connectionId, subscriptionId);
60
- if (timedOut && timedOut.status === "active") {
88
+ if (timedOut === subscription && timedOut.status === "active") {
61
89
  timedOut.status = "completed";
62
- console.warn(`Subscription ${subscriptionId} timed out after ${this.subscriptionTimeout}ms`);
90
+ logWarning(this.logger, `Subscription ${subscriptionId} timed out after ${this.subscriptionTimeout}ms`);
63
91
  onComplete();
64
92
  }
65
93
  } catch (error) {
66
- console.error(`Error completing timed-out subscription ${subscriptionId}:`, error);
94
+ this.logger.error(`Error completing timed-out subscription ${subscriptionId}:`, error);
67
95
  } finally {
68
- this.unsubscribe(connectionId, subscriptionId);
96
+ if (this.isCurrent(subscription)) {
97
+ this.unsubscribe(connectionId, subscriptionId);
98
+ }
69
99
  }
70
100
  }, this.subscriptionTimeout);
71
101
  }
@@ -74,9 +104,13 @@ class SubscriptionManager {
74
104
  clearTimeout(timeoutId);
75
105
  }
76
106
  };
77
- this.consumeIterator(connectionId, subscriptionId, iterator, onNext, onComplete, onError);
107
+ this.consumeIterator(subscription, iterator, onNext, onComplete, onError);
78
108
  return subscription;
79
109
  } catch (error) {
110
+ if (!this.isCurrent(subscription)) {
111
+ subscription.status = "completed";
112
+ return subscription;
113
+ }
80
114
  subscription.status = "error";
81
115
  const message = error instanceof Error ? error.message : "Subscription failed";
82
116
  onError([{ message }]);
@@ -84,24 +118,25 @@ class SubscriptionManager {
84
118
  return subscription;
85
119
  }
86
120
  }
87
- async consumeIterator(connectionId, subscriptionId, iterator, onNext, onComplete, onError) {
121
+ isCurrent(subscription) {
122
+ return this.getSubscription(subscription.connectionId, subscription.id) === subscription;
123
+ }
124
+ async consumeIterator(subscription, iterator, onNext, onComplete, onError) {
125
+ const { connectionId, id: subscriptionId } = subscription;
88
126
  try {
89
127
  for await (const result of iterator) {
90
- const subscription = this.getSubscription(connectionId, subscriptionId);
91
- if (!subscription || subscription.status !== "active") {
128
+ if (!this.isCurrent(subscription) || subscription.status !== "active") {
92
129
  break;
93
130
  }
94
131
  onNext(result);
95
132
  }
96
- const subscription = this.getSubscription(connectionId, subscriptionId);
97
- if (subscription) {
133
+ if (this.isCurrent(subscription)) {
98
134
  subscription.status = "completed";
99
135
  onComplete();
100
136
  this.unsubscribe(connectionId, subscriptionId);
101
137
  }
102
138
  } catch (error) {
103
- const subscription = this.getSubscription(connectionId, subscriptionId);
104
- if (subscription) {
139
+ if (this.isCurrent(subscription)) {
105
140
  subscription.status = "error";
106
141
  const message = error instanceof Error ? error.message : "Subscription error";
107
142
  onError([{ message }]);
@@ -260,15 +295,19 @@ class WebSocketHandler {
260
295
  onDisconnect;
261
296
  onSubscribe;
262
297
  onComplete;
298
+ logger;
263
299
  initTimeouts;
264
300
  keepAliveTimers;
301
+ states;
265
302
  constructor(config) {
266
303
  this.schema = config.schema;
267
304
  this.contextFactory = config.context;
268
305
  this.connectionInitTimeout = config.connectionInitTimeout ?? 3000;
269
306
  this.keepAliveInterval = config.keepAliveInterval ?? 12000;
307
+ this.logger = config.logger ?? console;
270
308
  this.subscriptionManager = new SubscriptionManager({
271
309
  schema: config.schema,
310
+ logger: this.logger,
272
311
  ...config.subscriptionManager
273
312
  });
274
313
  this.onConnect = config.onConnect;
@@ -277,6 +316,22 @@ class WebSocketHandler {
277
316
  this.onComplete = config.onComplete;
278
317
  this.initTimeouts = new Map;
279
318
  this.keepAliveTimers = new Map;
319
+ this.states = new WeakMap;
320
+ }
321
+ stateOf(socket) {
322
+ let state = this.states.get(socket);
323
+ if (!state) {
324
+ state = { initializing: false, closed: false, pending: new Map };
325
+ this.states.set(socket, state);
326
+ }
327
+ return state;
328
+ }
329
+ clearKeepAlive(connectionId) {
330
+ const timer = this.keepAliveTimers.get(connectionId);
331
+ if (timer) {
332
+ clearInterval(timer);
333
+ this.keepAliveTimers.delete(connectionId);
334
+ }
280
335
  }
281
336
  generateConnectionId() {
282
337
  return crypto.randomUUID();
@@ -286,11 +341,11 @@ class WebSocketHandler {
286
341
  const result = invoke();
287
342
  if (result instanceof Promise) {
288
343
  result.catch((error) => {
289
- console.error(`${name} failed:`, error);
344
+ this.logger.error(`${name} failed:`, error);
290
345
  });
291
346
  }
292
347
  } catch (error) {
293
- console.error(`${name} failed:`, error);
348
+ this.logger.error(`${name} failed:`, error);
294
349
  }
295
350
  }
296
351
  handleOpen(socket) {
@@ -300,6 +355,7 @@ class WebSocketHandler {
300
355
  initialized: false,
301
356
  subscriptions: new Set
302
357
  };
358
+ this.states.set(socket, { initializing: false, closed: false, pending: new Map });
303
359
  const timeout = setTimeout(() => {
304
360
  if (!socket.data.initialized) {
305
361
  socket.close(4408, "Connection initialization timeout");
@@ -344,10 +400,12 @@ class WebSocketHandler {
344
400
  }
345
401
  }
346
402
  async handleConnectionInit(socket, params) {
347
- if (socket.data.initialized) {
403
+ const state = this.stateOf(socket);
404
+ if (state.initializing || socket.data.initialized) {
348
405
  socket.close(4429, "Too many initialisation requests");
349
406
  return;
350
407
  }
408
+ state.initializing = true;
351
409
  const timeout = this.initTimeouts.get(socket.data.connectionId);
352
410
  if (timeout) {
353
411
  clearTimeout(timeout);
@@ -355,6 +413,9 @@ class WebSocketHandler {
355
413
  }
356
414
  if (this.onConnect) {
357
415
  const allowed = await this.onConnect(socket, params);
416
+ if (state.closed) {
417
+ return;
418
+ }
358
419
  if (!allowed) {
359
420
  socket.close(4403, "Forbidden");
360
421
  return;
@@ -364,6 +425,7 @@ class WebSocketHandler {
364
425
  socket.data.initialized = true;
365
426
  socket.send(formatMessage(createConnectionAck()));
366
427
  if (this.keepAliveInterval > 0) {
428
+ this.clearKeepAlive(socket.data.connectionId);
367
429
  const timer = setInterval(() => {
368
430
  socket.send(formatMessage({ type: "ping" /* Ping */ }));
369
431
  }, this.keepAliveInterval);
@@ -372,31 +434,54 @@ class WebSocketHandler {
372
434
  }
373
435
  async handleSubscribe(socket, message) {
374
436
  const { id, payload } = message;
437
+ const state = this.stateOf(socket);
375
438
  if (socket.data.subscriptions.has(id)) {
376
439
  socket.close(4409, `Subscriber for ${id} already exists`);
377
440
  return;
378
441
  }
442
+ if (socket.data.subscriptions.size >= this.subscriptionManager.subscriptionLimit) {
443
+ socket.send(formatMessage(createErrorMessage(id, [{ message: "Maximum subscriptions per connection reached" }])));
444
+ return;
445
+ }
379
446
  socket.data.subscriptions.add(id);
447
+ const token = { cancelled: false };
448
+ state.pending.set(id, token);
449
+ const settle = () => {
450
+ if (state.pending.get(id) === token) {
451
+ state.pending.delete(id);
452
+ }
453
+ };
380
454
  const request = {
381
455
  query: payload.query,
382
456
  operationName: payload.operationName,
383
457
  variables: payload.variables,
384
458
  extensions: payload.extensions
385
459
  };
460
+ const abandoned = () => state.closed || token.cancelled;
386
461
  try {
387
462
  await this.onSubscribe?.(socket, id, request);
463
+ if (abandoned())
464
+ return;
388
465
  let context;
389
466
  if (this.contextFactory) {
390
467
  context = await this.contextFactory(socket, request);
468
+ if (abandoned())
469
+ return;
391
470
  }
392
- if (!this.isSubscriptionOperation(request)) {
471
+ const isSubscription = await this.isSubscriptionOperation(request);
472
+ if (abandoned())
473
+ return;
474
+ if (!isSubscription) {
393
475
  const response = await this.subscriptionManager.execute(request, context);
476
+ if (abandoned())
477
+ return;
394
478
  if (response.data === undefined && response.errors && response.errors.length > 0) {
395
479
  socket.send(formatMessage(createErrorMessage(id, response.errors)));
396
480
  } else {
397
481
  socket.send(formatMessage(createNextMessage(id, response.data, response.errors)));
398
482
  socket.send(formatMessage(createCompleteMessage(id)));
399
483
  }
484
+ settle();
400
485
  socket.data.subscriptions.delete(id);
401
486
  this.invokeHook("onComplete hook", () => this.onComplete?.(socket, id));
402
487
  return;
@@ -412,35 +497,52 @@ class WebSocketHandler {
412
497
  socket.data.subscriptions.delete(id);
413
498
  this.invokeHook("onComplete hook", () => this.onComplete?.(socket, id));
414
499
  });
500
+ settle();
415
501
  } catch (error) {
502
+ if (abandoned())
503
+ return;
504
+ settle();
416
505
  const errorText = error instanceof Error ? error.message : "Subscription failed";
417
506
  socket.send(formatMessage(createErrorMessage(id, [{ message: errorText }])));
418
507
  socket.data.subscriptions.delete(id);
419
508
  this.invokeHook("onComplete hook", () => this.onComplete?.(socket, id));
420
509
  }
421
510
  }
422
- isSubscriptionOperation(request) {
423
- const document = parse(request.query);
511
+ async isSubscriptionOperation(request) {
512
+ let document;
513
+ try {
514
+ document = await this.subscriptionManager.parse(request.query);
515
+ } catch {
516
+ return true;
517
+ }
424
518
  const operation = getOperationAST(document, request.operationName ?? null);
425
519
  return !operation || operation.operation === OperationTypeNode.SUBSCRIPTION;
426
520
  }
427
521
  handleComplete(socket, subscriptionId) {
522
+ const state = this.stateOf(socket);
523
+ const pending = state.pending.get(subscriptionId);
524
+ if (pending) {
525
+ pending.cancelled = true;
526
+ state.pending.delete(subscriptionId);
527
+ }
428
528
  socket.data.subscriptions.delete(subscriptionId);
429
529
  this.subscriptionManager.unsubscribe(socket.data.connectionId, subscriptionId);
430
530
  this.invokeHook("onComplete hook", () => this.onComplete?.(socket, subscriptionId));
431
531
  }
432
532
  async handleClose(socket) {
433
533
  const { connectionId } = socket.data;
534
+ const state = this.stateOf(socket);
535
+ state.closed = true;
536
+ for (const pending of state.pending.values()) {
537
+ pending.cancelled = true;
538
+ }
539
+ state.pending.clear();
434
540
  const initTimeout = this.initTimeouts.get(connectionId);
435
541
  if (initTimeout) {
436
542
  clearTimeout(initTimeout);
437
543
  this.initTimeouts.delete(connectionId);
438
544
  }
439
- const keepAliveTimer = this.keepAliveTimers.get(connectionId);
440
- if (keepAliveTimer) {
441
- clearInterval(keepAliveTimer);
442
- this.keepAliveTimers.delete(connectionId);
443
- }
545
+ this.clearKeepAlive(connectionId);
444
546
  this.subscriptionManager.unsubscribeConnection(connectionId);
445
547
  socket.data.subscriptions.clear();
446
548
  await this.onDisconnect?.(socket);
@@ -450,12 +552,12 @@ class WebSocketHandler {
450
552
  open: (socket) => this.handleOpen(socket),
451
553
  message: (socket, message) => {
452
554
  this.handleMessage(socket, message).catch((error) => {
453
- console.error("WebSocket message handling failed:", error);
555
+ this.logger.error("WebSocket message handling failed:", error);
454
556
  });
455
557
  },
456
558
  close: (socket) => {
457
559
  this.handleClose(socket).catch((error) => {
458
- console.error("onDisconnect failed:", error);
560
+ this.logger.error("onDisconnect failed:", error);
459
561
  });
460
562
  }
461
563
  };
@@ -471,6 +573,7 @@ class PubSub {
471
573
  maxSubscribersPerTopic;
472
574
  wildcards;
473
575
  maxQueueSize;
576
+ logger;
474
577
  nextId;
475
578
  constructor(config = {}) {
476
579
  this.subscribers = new Map;
@@ -478,6 +581,7 @@ class PubSub {
478
581
  this.maxSubscribersPerTopic = config.maxSubscribersPerTopic ?? 1e4;
479
582
  this.wildcards = config.wildcards ?? false;
480
583
  this.maxQueueSize = config.maxQueueSize ?? Infinity;
584
+ this.logger = config.logger ?? console;
481
585
  this.nextId = 0;
482
586
  }
483
587
  subscribe(topic, callback) {
@@ -519,7 +623,7 @@ class PubSub {
519
623
  try {
520
624
  subscriber.callback(payload);
521
625
  } catch (error) {
522
- console.error("Error in subscriber callback:", error);
626
+ this.logger.error("Error in subscriber callback:", error);
523
627
  }
524
628
  }
525
629
  }
@@ -542,7 +646,7 @@ class PubSub {
542
646
  try {
543
647
  subscriber.callback(payload);
544
648
  } catch (error) {
545
- console.error("Error in wildcard subscriber callback:", error);
649
+ this.logger.error("Error in wildcard subscriber callback:", error);
546
650
  }
547
651
  }
548
652
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @leaven-graphql/ws - Logger interface
3
+ *
4
+ * Copyright 2026 Joseph Quinn
5
+ * Licensed under the Apache License, Version 2.0
6
+ */
7
+ /**
8
+ * Destination for the diagnostics the WebSocket transport emits: failing
9
+ * hooks, subscriber callbacks and subscription timeouts. Defaults to
10
+ * `console`. `warn` is optional; without it, warnings go to `error`.
11
+ */
12
+ export interface WebSocketLogger {
13
+ error(...args: unknown[]): void;
14
+ warn?(...args: unknown[]): void;
15
+ }
16
+ /**
17
+ * Emit a warning, falling back to `error` when the logger has no `warn`.
18
+ */
19
+ export declare function logWarning(logger: WebSocketLogger, ...args: unknown[]): void;
20
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChC,IAAI,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CACjC;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAM5E"}
package/dist/manager.d.ts CHANGED
@@ -4,8 +4,9 @@
4
4
  * Copyright 2026 Joseph Quinn
5
5
  * Licensed under the Apache License, Version 2.0
6
6
  */
7
- import type { GraphQLSchema } from 'graphql';
7
+ import type { DocumentNode, GraphQLSchema } from 'graphql';
8
8
  import { LeavenExecutor, type ExecutorConfig, type GraphQLRequest, type GraphQLResponse } from '@leaven-graphql/core';
9
+ import { type WebSocketLogger } from './logger';
9
10
  /**
10
11
  * Subscription status
11
12
  */
@@ -54,6 +55,11 @@ export interface SubscriptionManagerConfig {
54
55
  * Defaults to a new executor configured with `schema` alone.
55
56
  */
56
57
  executor?: LeavenExecutor | Omit<ExecutorConfig, 'schema'>;
58
+ /**
59
+ * Where diagnostics (subscription timeouts, failing completion callbacks)
60
+ * are logged. Defaults to `console`.
61
+ */
62
+ logger?: WebSocketLogger;
57
63
  }
58
64
  /**
59
65
  * Manages GraphQL subscriptions
@@ -73,7 +79,18 @@ export declare class SubscriptionManager {
73
79
  private readonly connections;
74
80
  private readonly maxSubscriptionsPerConnection;
75
81
  private readonly subscriptionTimeout;
82
+ private readonly logger;
76
83
  constructor(config: SubscriptionManagerConfig);
84
+ /**
85
+ * Maximum number of subscriptions (pending or active) one connection may
86
+ * hold.
87
+ */
88
+ get subscriptionLimit(): number;
89
+ /**
90
+ * Parse a query under the managed executor's parser limits.
91
+ * See {@link LeavenExecutor.parse}.
92
+ */
93
+ parse(query: string): Promise<DocumentNode>;
77
94
  /**
78
95
  * Execute a single-result operation (query or mutation) on the same
79
96
  * executor that serves subscriptions, so both transports honour the same
@@ -86,6 +103,12 @@ export declare class SubscriptionManager {
86
103
  subscribe<TContext = unknown>(connectionId: string, subscriptionId: string, request: GraphQLRequest, context: TContext, onNext: (result: GraphQLResponse) => void, onComplete: () => void, onError: (errors: readonly {
87
104
  message: string;
88
105
  }[]) => void): Promise<Subscription>;
106
+ /**
107
+ * Whether this exact subscription object is still the one registered under
108
+ * its connection and id. An id lookup alone is not enough: after a
109
+ * teardown the id can be registered again by a NEWER subscription.
110
+ */
111
+ private isCurrent;
89
112
  /**
90
113
  * Consume an async iterator and call callbacks
91
114
  */
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EACL,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAE9B;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;AAE9E;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,6BAA6B;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,oBAAoB;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,2BAA2B;IAC3B,OAAO,EAAE,cAAc,CAAC;IACxB,0BAA0B;IAC1B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAC;IAClD,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,qBAAqB;IACrB,MAAM,EAAE,aAAa,CAAC;IACtB,2CAA2C;IAC3C,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,2CAA2C;IAC3C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;CAC5D;AAED;;GAEG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyC;IACrE,OAAO,CAAC,QAAQ,CAAC,6BAA6B,CAAS;IACvD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;gBAEjC,MAAM,EAAE,yBAAyB;IAU7C;;;;OAIG;IACU,OAAO,CAAC,QAAQ,GAAG,OAAO,EACrC,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,eAAe,CAAC;IAK3B;;OAEG;IACU,SAAS,CAAC,QAAQ,GAAG,OAAO,EACvC,YAAY,EAAE,MAAM,EACpB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,QAAQ,EACjB,MAAM,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,EACzC,UAAU,EAAE,MAAM,IAAI,EACtB,OAAO,EAAE,CAAC,MAAM,EAAE,SAAS;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,KAAK,IAAI,GACxD,OAAO,CAAC,YAAY,CAAC;IAsGxB;;OAEG;YACW,eAAe;IAmC7B;;;;;;OAMG;IACI,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO;IAwBzE;;OAEG;IACI,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAiB1D;;OAEG;IACI,eAAe,CACpB,YAAY,EAAE,MAAM,EACpB,cAAc,EAAE,MAAM,GACrB,YAAY,GAAG,SAAS;IAI3B;;OAEG;IACI,0BAA0B,CAAC,YAAY,EAAE,MAAM,GAAG,YAAY,EAAE;IAKvE;;;OAGG;IACH,IAAW,iBAAiB,IAAI,MAAM,CAMrC;IAED;;OAEG;IACH,IAAW,eAAe,IAAI,MAAM,CAEnC;IAED;;OAEG;IACI,KAAK,IAAI,IAAI;CAMrB;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,yBAAyB,GAChC,mBAAmB,CAErB"}
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"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"}
package/dist/pubsub.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * Copyright 2026 Joseph Quinn
5
5
  * Licensed under the Apache License, Version 2.0
6
6
  */
7
+ import type { WebSocketLogger } from './logger';
7
8
  /**
8
9
  * Subscribe function type
9
10
  */
@@ -35,6 +36,8 @@ export interface PubSubConfig {
35
36
  * without bound. Unlimited by default.
36
37
  */
37
38
  maxQueueSize?: number;
39
+ /** Where subscriber callback errors are logged. Defaults to `console`. */
40
+ logger?: WebSocketLogger;
38
41
  }
39
42
  /**
40
43
  * In-memory PubSub implementation
@@ -46,6 +49,7 @@ export declare class PubSub implements PubSubEngine {
46
49
  private readonly maxSubscribersPerTopic;
47
50
  private readonly wildcards;
48
51
  private readonly maxQueueSize;
52
+ private readonly logger;
49
53
  private nextId;
50
54
  constructor(config?: PubSubConfig);
51
55
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"pubsub.d.ts","sourceRoot":"","sources":["../src/pubsub.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,OAAO,IAAI,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,KAC3B,MAAM,IAAI,CAAC;AAEhB;;GAEG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,WAAW,CAAC;IACvB,OAAO,EAAE,SAAS,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,6BAA6B;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAUD;;GAEG;AACH,qBAAa,MAAO,YAAW,YAAY;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,wEAAwE;IACxE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAwB;IACzD,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAS;IAChD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,GAAE,YAAiB;IASrC;;OAEG;IACI,SAAS,CAAC,CAAC,GAAG,OAAO,EAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,GAC7B,MAAM,IAAI;IA+Cb;;OAEG;IACI,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI;IAmB5D;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA2B1B;;OAEG;IACH,OAAO,CAAC,eAAe;IAwBvB;;OAEG;IACI,aAAa,CAAC,CAAC,GAAG,OAAO,EAC9B,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GACvB,qBAAqB,CAAC,CAAC,CAAC;IAmF3B;;OAEG;IACI,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAIhD;;OAEG;IACI,SAAS,IAAI,MAAM,EAAE;IAI5B;;OAEG;IACI,KAAK,IAAI,IAAI;CAIrB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,CAE1D"}
1
+ {"version":3,"file":"pubsub.d.ts","sourceRoot":"","sources":["../src/pubsub.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEhD;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,OAAO,IAAI,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,KAC3B,MAAM,IAAI,CAAC;AAEhB;;GAEG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,WAAW,CAAC;IACvB,OAAO,EAAE,SAAS,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,6BAA6B;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAUD;;GAEG;AACH,qBAAa,MAAO,YAAW,YAAY;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,wEAAwE;IACxE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAwB;IACzD,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAS;IAChD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,GAAE,YAAiB;IAUrC;;OAEG;IACI,SAAS,CAAC,CAAC,GAAG,OAAO,EAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,GAC7B,MAAM,IAAI;IA+Cb;;OAEG;IACI,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI;IAmB5D;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA2B1B;;OAEG;IACH,OAAO,CAAC,eAAe;IAwBvB;;OAEG;IACI,aAAa,CAAC,CAAC,GAAG,OAAO,EAC9B,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GACvB,qBAAqB,CAAC,CAAC,CAAC;IAmF3B;;OAEG;IACI,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAIhD;;OAEG;IACI,SAAS,IAAI,MAAM,EAAE;IAI5B;;OAEG;IACI,KAAK,IAAI,IAAI;CAIrB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,CAE1D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leaven-graphql/ws",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "WebSocket subscriptions support for Leaven GraphQL",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,9 +32,9 @@
32
32
  "directory": "packages/ws"
33
33
  },
34
34
  "dependencies": {
35
+ "@leaven-graphql/core": "0.2.2",
35
36
  "@leaven-graphql/context": "0.2.1",
36
- "@leaven-graphql/errors": "0.2.1",
37
- "@leaven-graphql/core": "0.2.1"
37
+ "@leaven-graphql/errors": "0.2.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/bun": "^1.3.14",