@leaven-graphql/ws 0.2.1 → 0.2.3

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
 
@@ -435,6 +466,10 @@ function createErrorMessage(
435
466
  function createCompleteMessage(id: string): CompleteMessage;
436
467
  function createPongMessage(payload?: Record<string, unknown>): PongMessage;
437
468
 
469
+ /** `/^[A-Za-z0-9_.:-]{1,64}$/`: the accepted shape of a client operation id */
470
+ const OPERATION_ID_PATTERN: RegExp;
471
+ function isValidOperationId(id: unknown): id is string;
472
+
438
473
  interface ParseMessageOptions {
439
474
  /**
440
475
  * Require a string `id` on the types that carry one (`subscribe`, `next`,
@@ -463,7 +498,11 @@ const message = parseMessage(data, { requireId: false });
463
498
 
464
499
  `parseMessage` throws on invalid JSON, on JSON that is not an object, on a
465
500
  missing or unknown `type`, and — unless `requireId` is `false` — on a missing
466
- `id`.
501
+ `id`, or on a `subscribe`/`complete` id that does not match
502
+ `OPERATION_ID_PATTERN` (`Invalid message`). The server closes the socket with
503
+ 4400 in every one of those cases. Ids are client-chosen and reach close
504
+ reasons and log lines, so the allow-list keeps control characters and
505
+ oversized strings out of both; UUIDs, counters and `prefix:n` ids all pass.
467
506
 
468
507
  ### Message Types
469
508
 
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":"AAAA;;;;;GAKG;AAEH,OAAO,EAA6C,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,KAAK,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAuB,KAAK,yBAAyB,EAAE,MAAM,WAAW,CAAC;AAchF;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oBAAoB;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,4CAA4C;IAC5C,WAAW,EAAE,OAAO,CAAC;IACrB,8BAA8B;IAC9B,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,MAAM,uBAAuB,CAAC,QAAQ,IAAI,CAC9C,MAAM,EAAE,eAAe,CAAC,gBAAgB,CAAC,EACzC,OAAO,EAAE,cAAc,KACpB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElC;;GAEG;AACH,MAAM,WAAW,sBAAsB,CAAC,QAAQ,GAAG,OAAO;IACxD,qBAAqB;IACrB,MAAM,EAAE,aAAa,CAAC;IACtB,sBAAsB;IACtB,OAAO,CAAC,EAAE,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAC5C,6CAA6C;IAC7C,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,+BAA+B;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kCAAkC;IAClC,mBAAmB,CAAC,EAAE,IAAI,CAAC,yBAAyB,EAAE,QAAQ,CAAC,CAAC;IAChE,8CAA8C;IAC9C,SAAS,CAAC,EAAE,CACV,MAAM,EAAE,eAAe,CAAC,gBAAgB,CAAC,EACzC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChC,yCAAyC;IACzC,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,CAAC,gBAAgB,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnF,mCAAmC;IACnC,WAAW,CAAC,EAAE,CACZ,MAAM,EAAE,eAAe,CAAC,gBAAgB,CAAC,EACzC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,cAAc,KACpB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,iCAAiC;IACjC,UAAU,CAAC,EAAE,CACX,MAAM,EAAE,eAAe,CAAC,gBAAgB,CAAC,EACzC,EAAE,EAAE,MAAM,KACP,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;;GAEG;AACH,qBAAa,gBAAgB,CAAC,QAAQ,GAAG,OAAO;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAoC;IACpE,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IAC/C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAsB;IAC1D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAgD;IAC3E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAmD;IACjF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAkD;IAC/E,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAiD;IAE7E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6C;IAC1E,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA8C;gBAElE,MAAM,EAAE,sBAAsB,CAAC,QAAQ,CAAC;IAkBpD;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAI5B;;;;;;;;OAQG;IACH,OAAO,CAAC,UAAU;IAalB;;OAEG;IACI,UAAU,CAAC,MAAM,EAAE,eAAe,CAAC,gBAAgB,CAAC,GAAG,IAAI;IAmBlE;;OAEG;IACU,aAAa,CACxB,MAAM,EAAE,eAAe,CAAC,gBAAgB,CAAC,EACzC,OAAO,EAAE,MAAM,GAAG,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC;IAYhB;;OAEG;YACW,cAAc;IAqC5B;;OAEG;YACW,oBAAoB;IA0ClC;;OAEG;YACW,eAAe;IA4F7B;;;;;;;OAOG;IACH,OAAO,CAAC,uBAAuB;IAM/B;;OAEG;IACH,OAAO,CAAC,cAAc;IAWtB;;OAEG;IACU,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,gBAAgB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBlF;;OAEG;IACI,kBAAkB,IAAI;QAC3B,IAAI,EAAE,CAAC,MAAM,EAAE,eAAe,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;QAC1D,OAAO,EAAE,CAAC,MAAM,EAAE,eAAe,CAAC,gBAAgB,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;QACvF,KAAK,EAAE,CAAC,MAAM,EAAE,eAAe,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;KAC5D;CAkBF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,GAAG,OAAO,EACvD,MAAM,EAAE,sBAAsB,CAAC,QAAQ,CAAC,GACvC,gBAAgB,CAAC,QAAQ,CAAC,CAE5B"}
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
- 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
+ export { MessageType, parseMessage, formatMessage, createConnectionAck, createNextMessage, createErrorMessage, createCompleteMessage, createPongMessage, isValidOperationId, OPERATION_ID_PATTERN, 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,kBAAkB,EAClB,oBAAoB,EACpB,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,41 @@
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
- LeavenExecutor
7
+ LeavenExecutor,
8
+ escapeLog
8
9
  } from "@leaven-graphql/core";
9
10
 
11
+ // src/logger.ts
12
+ function logWarning(logger, ...args) {
13
+ if (logger.warn) {
14
+ logger.warn(...args);
15
+ } else {
16
+ logger.error(...args);
17
+ }
18
+ }
19
+
20
+ // src/manager.ts
10
21
  class SubscriptionManager {
11
22
  executor;
12
23
  connections;
13
24
  maxSubscriptionsPerConnection;
14
25
  subscriptionTimeout;
26
+ logger;
15
27
  constructor(config) {
16
28
  this.executor = config.executor instanceof LeavenExecutor ? config.executor : new LeavenExecutor({ ...config.executor, schema: config.schema });
17
29
  this.connections = new Map;
18
30
  this.maxSubscriptionsPerConnection = config.maxSubscriptionsPerConnection ?? 100;
19
31
  this.subscriptionTimeout = config.subscriptionTimeout ?? 0;
32
+ this.logger = config.logger ?? console;
33
+ }
34
+ get subscriptionLimit() {
35
+ return this.maxSubscriptionsPerConnection;
36
+ }
37
+ parse(query) {
38
+ return this.executor.parse(query);
20
39
  }
21
40
  async execute(request, context) {
22
41
  const { response } = await this.executor.execute(request, context);
@@ -27,6 +46,9 @@ class SubscriptionManager {
27
46
  if (existing && existing.size >= this.maxSubscriptionsPerConnection) {
28
47
  throw new Error("Maximum subscriptions per connection reached");
29
48
  }
49
+ if (existing?.has(subscriptionId)) {
50
+ throw new Error(`Subscription ${escapeLog(subscriptionId)} already exists`);
51
+ }
30
52
  const subscription = {
31
53
  id: subscriptionId,
32
54
  connectionId,
@@ -42,6 +64,13 @@ class SubscriptionManager {
42
64
  connectionSubs.set(subscriptionId, subscription);
43
65
  try {
44
66
  const result = await this.executor.subscribe(request, context);
67
+ if (!this.isCurrent(subscription)) {
68
+ subscription.status = "completed";
69
+ if (Symbol.asyncIterator in Object(result)) {
70
+ result.return?.()?.catch(() => {});
71
+ }
72
+ return subscription;
73
+ }
45
74
  if (!(Symbol.asyncIterator in Object(result))) {
46
75
  subscription.status = "error";
47
76
  const errors = result.errors ?? [{ message: "Subscription failed" }];
@@ -57,15 +86,17 @@ class SubscriptionManager {
57
86
  timeoutId = setTimeout(() => {
58
87
  try {
59
88
  const timedOut = this.getSubscription(connectionId, subscriptionId);
60
- if (timedOut && timedOut.status === "active") {
89
+ if (timedOut === subscription && timedOut.status === "active") {
61
90
  timedOut.status = "completed";
62
- console.warn(`Subscription ${subscriptionId} timed out after ${this.subscriptionTimeout}ms`);
91
+ logWarning(this.logger, `Subscription ${escapeLog(subscriptionId)} timed out after ${this.subscriptionTimeout}ms`);
63
92
  onComplete();
64
93
  }
65
94
  } catch (error) {
66
- console.error(`Error completing timed-out subscription ${subscriptionId}:`, error);
95
+ this.logger.error(`Error completing timed-out subscription ${escapeLog(subscriptionId)}:`, error);
67
96
  } finally {
68
- this.unsubscribe(connectionId, subscriptionId);
97
+ if (this.isCurrent(subscription)) {
98
+ this.unsubscribe(connectionId, subscriptionId);
99
+ }
69
100
  }
70
101
  }, this.subscriptionTimeout);
71
102
  }
@@ -74,9 +105,13 @@ class SubscriptionManager {
74
105
  clearTimeout(timeoutId);
75
106
  }
76
107
  };
77
- this.consumeIterator(connectionId, subscriptionId, iterator, onNext, onComplete, onError);
108
+ this.consumeIterator(subscription, iterator, onNext, onComplete, onError);
78
109
  return subscription;
79
110
  } catch (error) {
111
+ if (!this.isCurrent(subscription)) {
112
+ subscription.status = "completed";
113
+ return subscription;
114
+ }
80
115
  subscription.status = "error";
81
116
  const message = error instanceof Error ? error.message : "Subscription failed";
82
117
  onError([{ message }]);
@@ -84,24 +119,25 @@ class SubscriptionManager {
84
119
  return subscription;
85
120
  }
86
121
  }
87
- async consumeIterator(connectionId, subscriptionId, iterator, onNext, onComplete, onError) {
122
+ isCurrent(subscription) {
123
+ return this.getSubscription(subscription.connectionId, subscription.id) === subscription;
124
+ }
125
+ async consumeIterator(subscription, iterator, onNext, onComplete, onError) {
126
+ const { connectionId, id: subscriptionId } = subscription;
88
127
  try {
89
128
  for await (const result of iterator) {
90
- const subscription = this.getSubscription(connectionId, subscriptionId);
91
- if (!subscription || subscription.status !== "active") {
129
+ if (!this.isCurrent(subscription) || subscription.status !== "active") {
92
130
  break;
93
131
  }
94
132
  onNext(result);
95
133
  }
96
- const subscription = this.getSubscription(connectionId, subscriptionId);
97
- if (subscription) {
134
+ if (this.isCurrent(subscription)) {
98
135
  subscription.status = "completed";
99
136
  onComplete();
100
137
  this.unsubscribe(connectionId, subscriptionId);
101
138
  }
102
139
  } catch (error) {
103
- const subscription = this.getSubscription(connectionId, subscriptionId);
104
- if (subscription) {
140
+ if (this.isCurrent(subscription)) {
105
141
  subscription.status = "error";
106
142
  const message = error instanceof Error ? error.message : "Subscription error";
107
143
  onError([{ message }]);
@@ -166,6 +202,7 @@ function createSubscriptionManager(config) {
166
202
  }
167
203
 
168
204
  // src/protocol.ts
205
+ import { escapeLog as escapeLog2 } from "@leaven-graphql/core";
169
206
  var MessageType;
170
207
  ((MessageType) => {
171
208
  MessageType["ConnectionInit"] = "connection_init";
@@ -184,6 +221,14 @@ var ID_REQUIRED_TYPES = new Set([
184
221
  "error" /* Error */,
185
222
  "complete" /* Complete */
186
223
  ]);
224
+ var CLIENT_ID_TYPES = new Set([
225
+ "subscribe" /* Subscribe */,
226
+ "complete" /* Complete */
227
+ ]);
228
+ var OPERATION_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,64}$/;
229
+ function isValidOperationId(id) {
230
+ return typeof id === "string" && OPERATION_ID_PATTERN.test(id);
231
+ }
187
232
  function parseMessage(data, options = {}) {
188
233
  const requireId = options.requireId ?? true;
189
234
  const text = typeof data === "string" ? data : data.toString("utf-8");
@@ -197,11 +242,14 @@ function parseMessage(data, options = {}) {
197
242
  throw new Error("Message must have a type");
198
243
  }
199
244
  if (typeof message.type !== "string" || !VALID_MESSAGE_TYPES.has(message.type)) {
200
- throw new Error(`Invalid message type: ${String(message.type)}`);
245
+ throw new Error(`Invalid message type: ${escapeLog2(message.type, 12)}`);
201
246
  }
202
247
  if (requireId && ID_REQUIRED_TYPES.has(message.type) && typeof message.id !== "string") {
203
248
  throw new Error(`Message of type "${message.type}" must have a string id`);
204
249
  }
250
+ if (requireId && CLIENT_ID_TYPES.has(message.type) && !isValidOperationId(message.id)) {
251
+ throw new Error("Invalid message");
252
+ }
205
253
  return message;
206
254
  } catch (error) {
207
255
  if (error instanceof SyntaxError) {
@@ -260,15 +308,19 @@ class WebSocketHandler {
260
308
  onDisconnect;
261
309
  onSubscribe;
262
310
  onComplete;
311
+ logger;
263
312
  initTimeouts;
264
313
  keepAliveTimers;
314
+ states;
265
315
  constructor(config) {
266
316
  this.schema = config.schema;
267
317
  this.contextFactory = config.context;
268
318
  this.connectionInitTimeout = config.connectionInitTimeout ?? 3000;
269
319
  this.keepAliveInterval = config.keepAliveInterval ?? 12000;
320
+ this.logger = config.logger ?? console;
270
321
  this.subscriptionManager = new SubscriptionManager({
271
322
  schema: config.schema,
323
+ logger: this.logger,
272
324
  ...config.subscriptionManager
273
325
  });
274
326
  this.onConnect = config.onConnect;
@@ -277,6 +329,22 @@ class WebSocketHandler {
277
329
  this.onComplete = config.onComplete;
278
330
  this.initTimeouts = new Map;
279
331
  this.keepAliveTimers = new Map;
332
+ this.states = new WeakMap;
333
+ }
334
+ stateOf(socket) {
335
+ let state = this.states.get(socket);
336
+ if (!state) {
337
+ state = { initializing: false, closed: false, pending: new Map };
338
+ this.states.set(socket, state);
339
+ }
340
+ return state;
341
+ }
342
+ clearKeepAlive(connectionId) {
343
+ const timer = this.keepAliveTimers.get(connectionId);
344
+ if (timer) {
345
+ clearInterval(timer);
346
+ this.keepAliveTimers.delete(connectionId);
347
+ }
280
348
  }
281
349
  generateConnectionId() {
282
350
  return crypto.randomUUID();
@@ -286,11 +354,11 @@ class WebSocketHandler {
286
354
  const result = invoke();
287
355
  if (result instanceof Promise) {
288
356
  result.catch((error) => {
289
- console.error(`${name} failed:`, error);
357
+ this.logger.error(`${name} failed:`, error);
290
358
  });
291
359
  }
292
360
  } catch (error) {
293
- console.error(`${name} failed:`, error);
361
+ this.logger.error(`${name} failed:`, error);
294
362
  }
295
363
  }
296
364
  handleOpen(socket) {
@@ -300,6 +368,7 @@ class WebSocketHandler {
300
368
  initialized: false,
301
369
  subscriptions: new Set
302
370
  };
371
+ this.states.set(socket, { initializing: false, closed: false, pending: new Map });
303
372
  const timeout = setTimeout(() => {
304
373
  if (!socket.data.initialized) {
305
374
  socket.close(4408, "Connection initialization timeout");
@@ -344,10 +413,12 @@ class WebSocketHandler {
344
413
  }
345
414
  }
346
415
  async handleConnectionInit(socket, params) {
347
- if (socket.data.initialized) {
416
+ const state = this.stateOf(socket);
417
+ if (state.initializing || socket.data.initialized) {
348
418
  socket.close(4429, "Too many initialisation requests");
349
419
  return;
350
420
  }
421
+ state.initializing = true;
351
422
  const timeout = this.initTimeouts.get(socket.data.connectionId);
352
423
  if (timeout) {
353
424
  clearTimeout(timeout);
@@ -355,6 +426,9 @@ class WebSocketHandler {
355
426
  }
356
427
  if (this.onConnect) {
357
428
  const allowed = await this.onConnect(socket, params);
429
+ if (state.closed) {
430
+ return;
431
+ }
358
432
  if (!allowed) {
359
433
  socket.close(4403, "Forbidden");
360
434
  return;
@@ -364,6 +438,7 @@ class WebSocketHandler {
364
438
  socket.data.initialized = true;
365
439
  socket.send(formatMessage(createConnectionAck()));
366
440
  if (this.keepAliveInterval > 0) {
441
+ this.clearKeepAlive(socket.data.connectionId);
367
442
  const timer = setInterval(() => {
368
443
  socket.send(formatMessage({ type: "ping" /* Ping */ }));
369
444
  }, this.keepAliveInterval);
@@ -372,31 +447,54 @@ class WebSocketHandler {
372
447
  }
373
448
  async handleSubscribe(socket, message) {
374
449
  const { id, payload } = message;
450
+ const state = this.stateOf(socket);
375
451
  if (socket.data.subscriptions.has(id)) {
376
452
  socket.close(4409, `Subscriber for ${id} already exists`);
377
453
  return;
378
454
  }
455
+ if (socket.data.subscriptions.size >= this.subscriptionManager.subscriptionLimit) {
456
+ socket.send(formatMessage(createErrorMessage(id, [{ message: "Maximum subscriptions per connection reached" }])));
457
+ return;
458
+ }
379
459
  socket.data.subscriptions.add(id);
460
+ const token = { cancelled: false };
461
+ state.pending.set(id, token);
462
+ const settle = () => {
463
+ if (state.pending.get(id) === token) {
464
+ state.pending.delete(id);
465
+ }
466
+ };
380
467
  const request = {
381
468
  query: payload.query,
382
469
  operationName: payload.operationName,
383
470
  variables: payload.variables,
384
471
  extensions: payload.extensions
385
472
  };
473
+ const abandoned = () => state.closed || token.cancelled;
386
474
  try {
387
475
  await this.onSubscribe?.(socket, id, request);
476
+ if (abandoned())
477
+ return;
388
478
  let context;
389
479
  if (this.contextFactory) {
390
480
  context = await this.contextFactory(socket, request);
481
+ if (abandoned())
482
+ return;
391
483
  }
392
- if (!this.isSubscriptionOperation(request)) {
484
+ const isSubscription = await this.isSubscriptionOperation(request);
485
+ if (abandoned())
486
+ return;
487
+ if (!isSubscription) {
393
488
  const response = await this.subscriptionManager.execute(request, context);
489
+ if (abandoned())
490
+ return;
394
491
  if (response.data === undefined && response.errors && response.errors.length > 0) {
395
492
  socket.send(formatMessage(createErrorMessage(id, response.errors)));
396
493
  } else {
397
494
  socket.send(formatMessage(createNextMessage(id, response.data, response.errors)));
398
495
  socket.send(formatMessage(createCompleteMessage(id)));
399
496
  }
497
+ settle();
400
498
  socket.data.subscriptions.delete(id);
401
499
  this.invokeHook("onComplete hook", () => this.onComplete?.(socket, id));
402
500
  return;
@@ -412,35 +510,52 @@ class WebSocketHandler {
412
510
  socket.data.subscriptions.delete(id);
413
511
  this.invokeHook("onComplete hook", () => this.onComplete?.(socket, id));
414
512
  });
513
+ settle();
415
514
  } catch (error) {
515
+ if (abandoned())
516
+ return;
517
+ settle();
416
518
  const errorText = error instanceof Error ? error.message : "Subscription failed";
417
519
  socket.send(formatMessage(createErrorMessage(id, [{ message: errorText }])));
418
520
  socket.data.subscriptions.delete(id);
419
521
  this.invokeHook("onComplete hook", () => this.onComplete?.(socket, id));
420
522
  }
421
523
  }
422
- isSubscriptionOperation(request) {
423
- const document = parse(request.query);
524
+ async isSubscriptionOperation(request) {
525
+ let document;
526
+ try {
527
+ document = await this.subscriptionManager.parse(request.query);
528
+ } catch {
529
+ return true;
530
+ }
424
531
  const operation = getOperationAST(document, request.operationName ?? null);
425
532
  return !operation || operation.operation === OperationTypeNode.SUBSCRIPTION;
426
533
  }
427
534
  handleComplete(socket, subscriptionId) {
535
+ const state = this.stateOf(socket);
536
+ const pending = state.pending.get(subscriptionId);
537
+ if (pending) {
538
+ pending.cancelled = true;
539
+ state.pending.delete(subscriptionId);
540
+ }
428
541
  socket.data.subscriptions.delete(subscriptionId);
429
542
  this.subscriptionManager.unsubscribe(socket.data.connectionId, subscriptionId);
430
543
  this.invokeHook("onComplete hook", () => this.onComplete?.(socket, subscriptionId));
431
544
  }
432
545
  async handleClose(socket) {
433
546
  const { connectionId } = socket.data;
547
+ const state = this.stateOf(socket);
548
+ state.closed = true;
549
+ for (const pending of state.pending.values()) {
550
+ pending.cancelled = true;
551
+ }
552
+ state.pending.clear();
434
553
  const initTimeout = this.initTimeouts.get(connectionId);
435
554
  if (initTimeout) {
436
555
  clearTimeout(initTimeout);
437
556
  this.initTimeouts.delete(connectionId);
438
557
  }
439
- const keepAliveTimer = this.keepAliveTimers.get(connectionId);
440
- if (keepAliveTimer) {
441
- clearInterval(keepAliveTimer);
442
- this.keepAliveTimers.delete(connectionId);
443
- }
558
+ this.clearKeepAlive(connectionId);
444
559
  this.subscriptionManager.unsubscribeConnection(connectionId);
445
560
  socket.data.subscriptions.clear();
446
561
  await this.onDisconnect?.(socket);
@@ -450,12 +565,12 @@ class WebSocketHandler {
450
565
  open: (socket) => this.handleOpen(socket),
451
566
  message: (socket, message) => {
452
567
  this.handleMessage(socket, message).catch((error) => {
453
- console.error("WebSocket message handling failed:", error);
568
+ this.logger.error("WebSocket message handling failed:", error);
454
569
  });
455
570
  },
456
571
  close: (socket) => {
457
572
  this.handleClose(socket).catch((error) => {
458
- console.error("onDisconnect failed:", error);
573
+ this.logger.error("onDisconnect failed:", error);
459
574
  });
460
575
  }
461
576
  };
@@ -465,12 +580,15 @@ function createWebSocketHandler(config) {
465
580
  return new WebSocketHandler(config);
466
581
  }
467
582
  // src/pubsub.ts
583
+ import { escapeLog as escapeLog3 } from "@leaven-graphql/core";
584
+
468
585
  class PubSub {
469
586
  subscribers;
470
587
  wildcardPatterns;
471
588
  maxSubscribersPerTopic;
472
589
  wildcards;
473
590
  maxQueueSize;
591
+ logger;
474
592
  nextId;
475
593
  constructor(config = {}) {
476
594
  this.subscribers = new Map;
@@ -478,6 +596,7 @@ class PubSub {
478
596
  this.maxSubscribersPerTopic = config.maxSubscribersPerTopic ?? 1e4;
479
597
  this.wildcards = config.wildcards ?? false;
480
598
  this.maxQueueSize = config.maxQueueSize ?? Infinity;
599
+ this.logger = config.logger ?? console;
481
600
  this.nextId = 0;
482
601
  }
483
602
  subscribe(topic, callback) {
@@ -493,7 +612,7 @@ class PubSub {
493
612
  }
494
613
  }
495
614
  if (topicSubscribers.size >= this.maxSubscribersPerTopic) {
496
- throw new Error(`Maximum subscribers reached for topic: ${topic}`);
615
+ throw new Error(`Maximum subscribers reached for topic: ${escapeLog3(topic)}`);
497
616
  }
498
617
  const subscriber = {
499
618
  id: String(this.nextId++),
@@ -519,7 +638,7 @@ class PubSub {
519
638
  try {
520
639
  subscriber.callback(payload);
521
640
  } catch (error) {
522
- console.error("Error in subscriber callback:", error);
641
+ this.logger.error("Error in subscriber callback:", error);
523
642
  }
524
643
  }
525
644
  }
@@ -542,7 +661,7 @@ class PubSub {
542
661
  try {
543
662
  subscriber.callback(payload);
544
663
  } catch (error) {
545
- console.error("Error in wildcard subscriber callback:", error);
664
+ this.logger.error("Error in wildcard subscriber callback:", error);
546
665
  }
547
666
  }
548
667
  }
@@ -645,6 +764,7 @@ function createPubSub(config) {
645
764
  }
646
765
  export {
647
766
  MessageType,
767
+ OPERATION_ID_PATTERN,
648
768
  PubSub,
649
769
  SubscriptionManager,
650
770
  WebSocketHandler,
@@ -657,5 +777,6 @@ export {
657
777
  createSubscriptionManager,
658
778
  createWebSocketHandler,
659
779
  formatMessage,
780
+ isValidOperationId,
660
781
  parseMessage
661
782
  };
@@ -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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"}
@@ -97,6 +97,22 @@ export interface CompleteMessage {
97
97
  * All message types
98
98
  */
99
99
  export type Message = ConnectionInitMessage | ConnectionAckMessage | PingMessage | PongMessage | SubscribeMessage | NextMessage | ErrorMessage | CompleteMessage;
100
+ /**
101
+ * Accepted shape of a client-chosen operation id: 1-64 characters from
102
+ * `[A-Za-z0-9_.:-]`.
103
+ *
104
+ * Ids are picked by the client and end up as map keys, in close reasons and
105
+ * in log lines. The graphql-ws spec only says "unique string", so in
106
+ * principle a client could send newlines, ANSI escapes or a megabyte of
107
+ * text; this allow-list covers UUIDs, counters and the `prefix:n` styles
108
+ * real clients use, and nothing that can forge a log line.
109
+ */
110
+ export declare const OPERATION_ID_PATTERN: RegExp;
111
+ /**
112
+ * Whether a value is an acceptable client-chosen operation id.
113
+ * See {@link OPERATION_ID_PATTERN}.
114
+ */
115
+ export declare function isValidOperationId(id: unknown): id is string;
100
116
  /**
101
117
  * Options for {@link parseMessage}
102
118
  */
@@ -113,6 +129,9 @@ export interface ParseMessageOptions {
113
129
  * lenient peer may omit the id on `next`/`error`, and a parse throw there is
114
130
  * far more disruptive (it tears down the whole connection) than tolerating
115
131
  * the frame.
132
+ *
133
+ * When `true`, the id of a `subscribe` or `complete` frame must also match
134
+ * {@link OPERATION_ID_PATTERN}; anything else throws `Invalid message`.
116
135
  */
117
136
  requireId?: boolean;
118
137
  }
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.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;AAGH,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.3",
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.3",
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",