@leaven-graphql/ws 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +326 -81
- package/dist/handler.d.ts +21 -2
- package/dist/handler.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4749 -4052
- package/dist/manager.d.ts +44 -9
- package/dist/manager.d.ts.map +1 -1
- package/dist/protocol.d.ts +30 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/pubsub.d.ts +13 -2
- package/dist/pubsub.d.ts.map +1 -1
- package/package.json +9 -6
package/LICENSE
CHANGED
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
|
|
176
176
|
END OF TERMS AND CONDITIONS
|
|
177
177
|
|
|
178
|
-
Copyright 2026
|
|
178
|
+
Copyright 2026 Joseph Quinn
|
|
179
179
|
|
|
180
180
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
181
|
you may not use this file except in compliance with the License.
|
package/README.md
CHANGED
|
@@ -10,33 +10,60 @@ bun add @leaven-graphql/ws @leaven-graphql/core graphql
|
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
|
+
`createWebSocketHandler` implements the whole `graphql-ws` server: connection
|
|
14
|
+
init and acknowledgement, keep-alive pings, subscription lifecycle, and the
|
|
15
|
+
protocol close codes. Hand it to `Bun.serve` via `getWebSocketConfig()`.
|
|
16
|
+
|
|
13
17
|
```typescript
|
|
14
|
-
import {
|
|
15
|
-
import { LeavenExecutor } from '@leaven-graphql/core';
|
|
18
|
+
import { createWebSocketHandler, type WebSocketContext } from '@leaven-graphql/ws';
|
|
16
19
|
|
|
17
|
-
const
|
|
18
|
-
|
|
20
|
+
const handler = createWebSocketHandler({
|
|
21
|
+
schema,
|
|
22
|
+
// Optional: build the GraphQL context for every operation
|
|
23
|
+
context: (socket) => ({ user: socket.data.connectionParams?.user }),
|
|
24
|
+
});
|
|
19
25
|
|
|
20
26
|
Bun.serve({
|
|
21
27
|
port: 4000,
|
|
22
28
|
fetch(request, server) {
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
// Bun's types require the `data` option, but the handler's `open` replaces
|
|
30
|
+
// socket.data with a fresh WebSocketContext, so nothing needs stashing.
|
|
31
|
+
if (server.upgrade(request, { data: undefined as unknown as WebSocketContext })) {
|
|
25
32
|
return;
|
|
26
33
|
}
|
|
27
34
|
return new Response('Not Found', { status: 404 });
|
|
28
35
|
},
|
|
29
|
-
websocket:
|
|
30
|
-
message(ws, message) {
|
|
31
|
-
handleMessage(ws, message, executor, pubsub);
|
|
32
|
-
},
|
|
33
|
-
close(ws) {
|
|
34
|
-
cleanupConnection(ws);
|
|
35
|
-
},
|
|
36
|
-
},
|
|
36
|
+
websocket: handler.getWebSocketConfig(),
|
|
37
37
|
});
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Queries and mutations are supported on the same transport: per the `graphql-ws`
|
|
41
|
+
protocol, a `subscribe` frame carrying a query or mutation is answered with a
|
|
42
|
+
single `next` followed by `complete`. Failures before execution (validation,
|
|
43
|
+
variable coercion) come back as an `error` frame instead; resolver errors ride
|
|
44
|
+
inside the `next` payload.
|
|
45
|
+
|
|
46
|
+
Serving HTTP and WebSocket from one server is a matter of passing the handler
|
|
47
|
+
to `@leaven-graphql/http`. Subscriptions do **not** inherit the HTTP
|
|
48
|
+
executor's options, so state them for both transports — otherwise the
|
|
49
|
+
longest-lived transport quietly runs on executor defaults:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { createServer } from '@leaven-graphql/http';
|
|
53
|
+
import { createWebSocketHandler } from '@leaven-graphql/ws';
|
|
54
|
+
|
|
55
|
+
const executor = { introspection: false, maxDepth: 10 };
|
|
56
|
+
|
|
57
|
+
createServer({
|
|
58
|
+
schema,
|
|
59
|
+
executor,
|
|
60
|
+
websocket: createWebSocketHandler({
|
|
61
|
+
schema,
|
|
62
|
+
subscriptionManager: { executor },
|
|
63
|
+
}),
|
|
64
|
+
}).start();
|
|
65
|
+
```
|
|
66
|
+
|
|
40
67
|
## Features
|
|
41
68
|
|
|
42
69
|
### PubSub
|
|
@@ -114,62 +141,35 @@ const resolvers = {
|
|
|
114
141
|
|
|
115
142
|
### Authentication
|
|
116
143
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const connections = new Map();
|
|
121
|
-
|
|
122
|
-
Bun.serve({
|
|
123
|
-
websocket: {
|
|
124
|
-
async message(ws, data) {
|
|
125
|
-
const message = parseMessage(data);
|
|
126
|
-
|
|
127
|
-
switch (message.type) {
|
|
128
|
-
case 'connection_init': {
|
|
129
|
-
const token = message.payload?.authToken;
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
const user = await verifyToken(token);
|
|
133
|
-
connections.set(ws, { user, subscriptions: new Map() });
|
|
134
|
-
ws.send(JSON.stringify({ type: 'connection_ack' }));
|
|
135
|
-
} catch (error) {
|
|
136
|
-
ws.close(4401, 'Unauthorized');
|
|
137
|
-
}
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
case 'subscribe': {
|
|
142
|
-
const connection = connections.get(ws);
|
|
143
|
-
if (!connection) {
|
|
144
|
-
ws.close(4401, 'Unauthorized');
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
handleSubscribe(ws, message, connection.user);
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
144
|
+
Authenticate in `onConnect`. Returning `false` closes the socket with `4403`,
|
|
145
|
+
and no `subscribe` frame is accepted before a connection is acknowledged
|
|
146
|
+
(unacknowledged operations are closed with `4401`).
|
|
150
147
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
148
|
+
```typescript
|
|
149
|
+
import { createWebSocketHandler } from '@leaven-graphql/ws';
|
|
150
|
+
|
|
151
|
+
const users = new WeakMap<object, User>();
|
|
152
|
+
|
|
153
|
+
const handler = createWebSocketHandler({
|
|
154
|
+
schema,
|
|
155
|
+
async onConnect(socket, params) {
|
|
156
|
+
try {
|
|
157
|
+
users.set(socket, await verifyToken(params?.authToken as string));
|
|
158
|
+
return true;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
context: (socket) => ({ user: users.get(socket) }),
|
|
164
|
+
onDisconnect(socket) {
|
|
165
|
+
users.delete(socket);
|
|
169
166
|
},
|
|
170
167
|
});
|
|
171
168
|
```
|
|
172
169
|
|
|
170
|
+
The lifecycle hooks may be async; a hook that rejects is logged and never
|
|
171
|
+
takes the connection (or the process) down with it.
|
|
172
|
+
|
|
173
173
|
### Schema Definition
|
|
174
174
|
|
|
175
175
|
```graphql
|
|
@@ -239,44 +239,289 @@ unsubscribe();
|
|
|
239
239
|
|
|
240
240
|
## API Reference
|
|
241
241
|
|
|
242
|
+
### WebSocketHandler
|
|
243
|
+
|
|
244
|
+
The `graphql-ws` server. `getWebSocketConfig()` returns the object `Bun.serve`
|
|
245
|
+
expects under `websocket`.
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
class WebSocketHandler<TContext = unknown> {
|
|
249
|
+
constructor(config: WebSocketHandlerConfig<TContext>);
|
|
250
|
+
|
|
251
|
+
handleOpen(socket: ServerWebSocket<WebSocketContext>): void;
|
|
252
|
+
handleMessage(
|
|
253
|
+
socket: ServerWebSocket<WebSocketContext>,
|
|
254
|
+
message: string | Buffer
|
|
255
|
+
): Promise<void>;
|
|
256
|
+
handleClose(socket: ServerWebSocket<WebSocketContext>): Promise<void>;
|
|
257
|
+
|
|
258
|
+
getWebSocketConfig(): {
|
|
259
|
+
open: (socket: ServerWebSocket<WebSocketContext>) => void;
|
|
260
|
+
message: (
|
|
261
|
+
socket: ServerWebSocket<WebSocketContext>,
|
|
262
|
+
message: string | Buffer
|
|
263
|
+
) => void;
|
|
264
|
+
close: (socket: ServerWebSocket<WebSocketContext>) => void;
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function createWebSocketHandler<TContext = unknown>(
|
|
269
|
+
config: WebSocketHandlerConfig<TContext>
|
|
270
|
+
): WebSocketHandler<TContext>;
|
|
271
|
+
|
|
272
|
+
interface WebSocketHandlerConfig<TContext = unknown> {
|
|
273
|
+
/** GraphQL schema */
|
|
274
|
+
schema: GraphQLSchema;
|
|
275
|
+
/** Builds the GraphQL context for each operation */
|
|
276
|
+
context?: (
|
|
277
|
+
socket: ServerWebSocket<WebSocketContext>,
|
|
278
|
+
request: GraphQLRequest
|
|
279
|
+
) => TContext | Promise<TContext>;
|
|
280
|
+
/** Close with 4408 if `connection_init` does not arrive in time (default 3000ms) */
|
|
281
|
+
connectionInitTimeout?: number;
|
|
282
|
+
/** Server ping interval; 0 disables keep-alive (default 12000ms) */
|
|
283
|
+
keepAliveInterval?: number;
|
|
284
|
+
/** Forwarded to the SubscriptionManager (minus `schema`) */
|
|
285
|
+
subscriptionManager?: Omit<SubscriptionManagerConfig, 'schema'>;
|
|
286
|
+
/** Return false to reject the connection with 4403 */
|
|
287
|
+
onConnect?: (
|
|
288
|
+
socket: ServerWebSocket<WebSocketContext>,
|
|
289
|
+
params?: Record<string, unknown>
|
|
290
|
+
) => boolean | Promise<boolean>;
|
|
291
|
+
onDisconnect?: (socket: ServerWebSocket<WebSocketContext>) => void | Promise<void>;
|
|
292
|
+
onSubscribe?: (
|
|
293
|
+
socket: ServerWebSocket<WebSocketContext>,
|
|
294
|
+
id: string,
|
|
295
|
+
request: GraphQLRequest
|
|
296
|
+
) => void | Promise<void>;
|
|
297
|
+
/** Called once per operation id when it reaches a terminal state */
|
|
298
|
+
onComplete?: (
|
|
299
|
+
socket: ServerWebSocket<WebSocketContext>,
|
|
300
|
+
id: string
|
|
301
|
+
) => void | Promise<void>;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
interface WebSocketContext {
|
|
305
|
+
/** Opaque UUIDv4 — do not parse it */
|
|
306
|
+
connectionId: string;
|
|
307
|
+
connectionParams?: Record<string, unknown>;
|
|
308
|
+
initialized: boolean;
|
|
309
|
+
subscriptions: Set<string>;
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Close codes follow the protocol: `4400` invalid message, `4401` unauthorized
|
|
314
|
+
(operation before `connection_init`), `4403` rejected by `onConnect`, `4408`
|
|
315
|
+
init timeout, `4409` duplicate subscription id, `4429` repeated
|
|
316
|
+
`connection_init`.
|
|
317
|
+
|
|
318
|
+
### SubscriptionManager
|
|
319
|
+
|
|
320
|
+
Owns the executor and the per-connection subscription registry. The handler
|
|
321
|
+
creates one; you only need it directly when driving subscriptions yourself.
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
class SubscriptionManager {
|
|
325
|
+
constructor(config: SubscriptionManagerConfig);
|
|
326
|
+
|
|
327
|
+
subscribe<TContext = unknown>(
|
|
328
|
+
connectionId: string,
|
|
329
|
+
subscriptionId: string,
|
|
330
|
+
request: GraphQLRequest,
|
|
331
|
+
context: TContext,
|
|
332
|
+
onNext: (result: GraphQLResponse) => void,
|
|
333
|
+
onComplete: () => void,
|
|
334
|
+
onError: (errors: readonly { message: string }[]) => void
|
|
335
|
+
): Promise<Subscription>;
|
|
336
|
+
|
|
337
|
+
/** Run a query or mutation on the same executor */
|
|
338
|
+
execute<TContext = unknown>(
|
|
339
|
+
request: GraphQLRequest,
|
|
340
|
+
context?: TContext
|
|
341
|
+
): Promise<GraphQLResponse>;
|
|
342
|
+
|
|
343
|
+
unsubscribe(connectionId: string, subscriptionId: string): boolean;
|
|
344
|
+
unsubscribeConnection(connectionId: string): number;
|
|
345
|
+
getSubscription(connectionId: string, subscriptionId: string): Subscription | undefined;
|
|
346
|
+
getConnectionSubscriptions(connectionId: string): Subscription[];
|
|
347
|
+
clear(): void;
|
|
348
|
+
|
|
349
|
+
readonly subscriptionCount: number;
|
|
350
|
+
readonly connectionCount: number;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function createSubscriptionManager(config: SubscriptionManagerConfig): SubscriptionManager;
|
|
354
|
+
|
|
355
|
+
interface SubscriptionManagerConfig {
|
|
356
|
+
schema: GraphQLSchema;
|
|
357
|
+
/** Default 100 */
|
|
358
|
+
maxSubscriptionsPerConnection?: number;
|
|
359
|
+
/** Complete a subscription after this many ms; 0 disables (default 0) */
|
|
360
|
+
subscriptionTimeout?: number;
|
|
361
|
+
/**
|
|
362
|
+
* Reuse an existing executor, or configure the one this manager builds.
|
|
363
|
+
* Sharing an executor with the HTTP transport avoids a second schema print
|
|
364
|
+
* and a duplicate document cache, and is the only way subscriptions inherit
|
|
365
|
+
* `introspection: false`, `maxDepth` or `maxComplexity`.
|
|
366
|
+
*/
|
|
367
|
+
executor?: LeavenExecutor | Omit<ExecutorConfig, 'schema'>;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
interface Subscription {
|
|
371
|
+
id: string;
|
|
372
|
+
connectionId: string;
|
|
373
|
+
request: GraphQLRequest;
|
|
374
|
+
status: 'pending' | 'active' | 'completed' | 'error';
|
|
375
|
+
createdAt: number;
|
|
376
|
+
iterator?: AsyncIterableIterator<GraphQLResponse>;
|
|
377
|
+
cleanup?: () => void;
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Subscription ids are scoped to a connection: `graphql-ws` clients number their
|
|
382
|
+
operations per connection (starting at `"1"`), so every lookup and teardown
|
|
383
|
+
takes **both** the connection id and the subscription id.
|
|
384
|
+
|
|
242
385
|
### PubSub
|
|
243
386
|
|
|
244
387
|
```typescript
|
|
245
|
-
class PubSub {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
388
|
+
class PubSub implements PubSubEngine {
|
|
389
|
+
constructor(config?: PubSubConfig);
|
|
390
|
+
|
|
391
|
+
subscribe<T = unknown>(topic: string, callback: (payload: T) => void): () => void;
|
|
392
|
+
publish<T = unknown>(topic: string, payload: T): void;
|
|
393
|
+
asyncIterator<T = unknown>(topic: string | string[]): AsyncIterableIterator<T>;
|
|
394
|
+
getSubscriberCount(topic: string): number;
|
|
395
|
+
getTopics(): string[];
|
|
396
|
+
clear(): void;
|
|
249
397
|
}
|
|
250
398
|
|
|
251
399
|
function createPubSub(config?: PubSubConfig): PubSub;
|
|
252
400
|
|
|
253
401
|
interface PubSubConfig {
|
|
254
|
-
|
|
402
|
+
/** Subscribers allowed per topic before `subscribe` throws (default 10000) */
|
|
403
|
+
maxSubscribersPerTopic?: number;
|
|
404
|
+
/** Enable `.`-segmented `*` / `#` topic patterns (default false) */
|
|
255
405
|
wildcards?: boolean;
|
|
406
|
+
/**
|
|
407
|
+
* Payloads an `asyncIterator` buffers while nothing is pulling. When full,
|
|
408
|
+
* the OLDEST payload is dropped so a slow consumer sees the most recent
|
|
409
|
+
* events instead of an unbounded queue. Unlimited by default.
|
|
410
|
+
*/
|
|
411
|
+
maxQueueSize?: number;
|
|
256
412
|
}
|
|
257
413
|
```
|
|
258
414
|
|
|
259
415
|
### Message Handling
|
|
260
416
|
|
|
417
|
+
`parseMessage` returns a parsed message object; `formatMessage` takes a message
|
|
418
|
+
object and returns the string to send. The `create*` helpers build message
|
|
419
|
+
objects, so they are composed with `formatMessage` rather than sent directly.
|
|
420
|
+
|
|
261
421
|
```typescript
|
|
262
|
-
function parseMessage(data: string | Buffer):
|
|
263
|
-
function formatMessage(
|
|
264
|
-
|
|
265
|
-
function
|
|
266
|
-
function
|
|
422
|
+
function parseMessage(data: string | Buffer, options?: ParseMessageOptions): Message;
|
|
423
|
+
function formatMessage(message: Message): string;
|
|
424
|
+
|
|
425
|
+
function createConnectionAck(payload?: Record<string, unknown>): ConnectionAckMessage;
|
|
426
|
+
function createNextMessage(
|
|
427
|
+
id: string,
|
|
428
|
+
data: Record<string, unknown> | null | undefined,
|
|
429
|
+
errors?: readonly { message: string }[]
|
|
430
|
+
): NextMessage;
|
|
431
|
+
function createErrorMessage(
|
|
432
|
+
id: string,
|
|
433
|
+
errors: readonly { message: string }[]
|
|
434
|
+
): ErrorMessage;
|
|
435
|
+
function createCompleteMessage(id: string): CompleteMessage;
|
|
436
|
+
function createPongMessage(payload?: Record<string, unknown>): PongMessage;
|
|
437
|
+
|
|
438
|
+
interface ParseMessageOptions {
|
|
439
|
+
/**
|
|
440
|
+
* Require a string `id` on the types that carry one (`subscribe`, `next`,
|
|
441
|
+
* `error`, `complete`). Defaults to `true`, which is correct for a server
|
|
442
|
+
* reading client frames: an operation frame with no id cannot be routed.
|
|
443
|
+
* Pass `false` on the client, where a lenient peer may omit the id and a
|
|
444
|
+
* throw would tear down the whole connection.
|
|
445
|
+
*/
|
|
446
|
+
requireId?: boolean;
|
|
447
|
+
}
|
|
267
448
|
```
|
|
268
449
|
|
|
450
|
+
```typescript
|
|
451
|
+
import {
|
|
452
|
+
createCompleteMessage,
|
|
453
|
+
createNextMessage,
|
|
454
|
+
formatMessage,
|
|
455
|
+
parseMessage,
|
|
456
|
+
} from '@leaven-graphql/ws';
|
|
457
|
+
|
|
458
|
+
socket.send(formatMessage(createNextMessage('1', { hello: 'world' })));
|
|
459
|
+
socket.send(formatMessage(createCompleteMessage('1')));
|
|
460
|
+
|
|
461
|
+
const message = parseMessage(data, { requireId: false });
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
`parseMessage` throws on invalid JSON, on JSON that is not an object, on a
|
|
465
|
+
missing or unknown `type`, and — unless `requireId` is `false` — on a missing
|
|
466
|
+
`id`.
|
|
467
|
+
|
|
269
468
|
### Message Types
|
|
270
469
|
|
|
271
470
|
```typescript
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
471
|
+
enum MessageType {
|
|
472
|
+
ConnectionInit = 'connection_init',
|
|
473
|
+
ConnectionAck = 'connection_ack',
|
|
474
|
+
Ping = 'ping',
|
|
475
|
+
Pong = 'pong',
|
|
476
|
+
Subscribe = 'subscribe',
|
|
477
|
+
Next = 'next',
|
|
478
|
+
Error = 'error',
|
|
479
|
+
Complete = 'complete',
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
type Message =
|
|
483
|
+
| ConnectionInitMessage
|
|
484
|
+
| ConnectionAckMessage
|
|
485
|
+
| PingMessage
|
|
486
|
+
| PongMessage
|
|
487
|
+
| SubscribeMessage
|
|
488
|
+
| NextMessage
|
|
489
|
+
| ErrorMessage
|
|
490
|
+
| CompleteMessage;
|
|
491
|
+
|
|
492
|
+
interface SubscribeMessage {
|
|
493
|
+
id: string;
|
|
494
|
+
type: MessageType.Subscribe;
|
|
495
|
+
payload: {
|
|
496
|
+
operationName?: string;
|
|
497
|
+
query: string;
|
|
498
|
+
variables?: Record<string, unknown>;
|
|
499
|
+
extensions?: Record<string, unknown>;
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
interface NextMessage {
|
|
504
|
+
id: string;
|
|
505
|
+
type: MessageType.Next;
|
|
506
|
+
payload: {
|
|
507
|
+
data?: Record<string, unknown> | null;
|
|
508
|
+
errors?: readonly { message: string; [key: string]: unknown }[];
|
|
509
|
+
extensions?: Record<string, unknown>;
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
interface ErrorMessage {
|
|
514
|
+
id: string;
|
|
515
|
+
type: MessageType.Error;
|
|
516
|
+
payload: readonly { message: string; [key: string]: unknown }[];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
interface CompleteMessage {
|
|
520
|
+
id: string;
|
|
521
|
+
type: MessageType.Complete;
|
|
277
522
|
}
|
|
278
523
|
```
|
|
279
524
|
|
|
280
525
|
## License
|
|
281
526
|
|
|
282
|
-
Apache 2.0 -
|
|
527
|
+
Apache 2.0 - Joseph Quinn
|
package/dist/handler.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @leaven-graphql/ws - WebSocket handler
|
|
3
3
|
*
|
|
4
|
-
* Copyright 2026
|
|
4
|
+
* Copyright 2026 Joseph Quinn
|
|
5
5
|
* Licensed under the Apache License, Version 2.0
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
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';
|
|
@@ -68,6 +68,16 @@ export declare class WebSocketHandler<TContext = unknown> {
|
|
|
68
68
|
* Generate a unique connection ID
|
|
69
69
|
*/
|
|
70
70
|
private generateConnectionId;
|
|
71
|
+
/**
|
|
72
|
+
* Invoke a user-supplied lifecycle hook without letting its failure escape.
|
|
73
|
+
*
|
|
74
|
+
* These hooks are called from places with no caller able to catch: iterator
|
|
75
|
+
* consumption stacks inside the subscription manager, timer callbacks, and
|
|
76
|
+
* Bun's `void`-returning socket callbacks. A rejected promise dropped into
|
|
77
|
+
* one of those slots is an unhandled rejection, which by default aborts the
|
|
78
|
+
* process and takes every other connection with it.
|
|
79
|
+
*/
|
|
80
|
+
private invokeHook;
|
|
71
81
|
/**
|
|
72
82
|
* Handle WebSocket open
|
|
73
83
|
*/
|
|
@@ -88,6 +98,15 @@ export declare class WebSocketHandler<TContext = unknown> {
|
|
|
88
98
|
* Handle subscribe message
|
|
89
99
|
*/
|
|
90
100
|
private handleSubscribe;
|
|
101
|
+
/**
|
|
102
|
+
* Whether a request's selected operation is a subscription.
|
|
103
|
+
*
|
|
104
|
+
* A document graphql-js cannot resolve to a single operation (no operations
|
|
105
|
+
* at all, or several with no `operationName`) is reported as a subscription
|
|
106
|
+
* so the executor produces the precise error rather than this method
|
|
107
|
+
* guessing at one.
|
|
108
|
+
*/
|
|
109
|
+
private isSubscriptionOperation;
|
|
91
110
|
/**
|
|
92
111
|
* Handle complete message (client unsubscribe)
|
|
93
112
|
*/
|
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,KAAK,
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @leaven-graphql/ws - WebSocket subscriptions for Leaven
|
|
3
3
|
*
|
|
4
|
-
* Copyright 2026
|
|
4
|
+
* Copyright 2026 Joseph Quinn
|
|
5
5
|
* Licensed under the Apache License, Version 2.0
|
|
6
6
|
*/
|
|
7
7
|
export { WebSocketHandler, createWebSocketHandler } from './handler';
|
|
@@ -10,5 +10,5 @@ export { SubscriptionManager, createSubscriptionManager } from './manager';
|
|
|
10
10
|
export type { SubscriptionManagerConfig, Subscription, SubscriptionStatus } from './manager';
|
|
11
11
|
export { PubSub, createPubSub } from './pubsub';
|
|
12
12
|
export type { PubSubConfig, PubSubEngine, SubscribeFn, PublishFn } from './pubsub';
|
|
13
|
-
export { MessageType, parseMessage, formatMessage, type ConnectionInitMessage, type ConnectionAckMessage, type SubscribeMessage, type NextMessage, type ErrorMessage, type CompleteMessage, type PingMessage, type PongMessage, } from './protocol';
|
|
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
14
|
//# 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,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,WAAW,
|
|
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"}
|