@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 +33 -2
- package/dist/handler.d.ts +22 -1
- package/dist/handler.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +132 -28
- package/dist/logger.d.ts +20 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/manager.d.ts +24 -1
- package/dist/manager.d.ts.map +1 -1
- package/dist/pubsub.d.ts +4 -0
- package/dist/pubsub.d.ts.map +1 -1
- package/package.json +3 -3
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
|
/**
|
package/dist/handler.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
-
|
|
90
|
+
logWarning(this.logger, `Subscription ${subscriptionId} timed out after ${this.subscriptionTimeout}ms`);
|
|
63
91
|
onComplete();
|
|
64
92
|
}
|
|
65
93
|
} catch (error) {
|
|
66
|
-
|
|
94
|
+
this.logger.error(`Error completing timed-out subscription ${subscriptionId}:`, error);
|
|
67
95
|
} finally {
|
|
68
|
-
this.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
344
|
+
this.logger.error(`${name} failed:`, error);
|
|
290
345
|
});
|
|
291
346
|
}
|
|
292
347
|
} catch (error) {
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
649
|
+
this.logger.error("Error in wildcard subscriber callback:", error);
|
|
546
650
|
}
|
|
547
651
|
}
|
|
548
652
|
}
|
package/dist/logger.d.ts
ADDED
|
@@ -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
|
*/
|
package/dist/manager.d.ts.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC3D,OAAO,EACL,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAE5D;;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;IAC3D;;;OAGG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;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;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;gBAE7B,MAAM,EAAE,yBAAyB;IAW7C;;;OAGG;IACH,IAAW,iBAAiB,IAAI,MAAM,CAErC;IAED;;;OAGG;IACI,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAIlD;;;;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;IAkIxB;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAIjB;;OAEG;YACW,eAAe;IAkC7B;;;;;;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"}
|
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
|
/**
|
package/dist/pubsub.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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",
|