Skip to content
This repository was archived by the owner on May 17, 2025. It is now read-only.

Commit b8abad3

Browse files
authored
feat: add subscribe and complete to the context, improve their docs (#56)
1 parent 50446cf commit b8abad3

File tree

11 files changed

+78
-85
lines changed

11 files changed

+78
-85
lines changed

README.md

Lines changed: 48 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const subscriptionServer = makeServer({
4747
### Export the handler
4848

4949
```ts
50-
export const handler = subscriptionServer.webSocketHandler;
50+
export const handler = subscriptionServer.webSocketHandler
5151
```
5252

5353
### Configure API Gateway
@@ -86,7 +86,7 @@ functions:
8686
8787
</details>
8888
89-
### Create DynanmoDB tables for state
89+
### Create DynamoDB tables for state
9090
9191
In-flight connections and subscriptions need to be persisted.
9292
@@ -305,7 +305,7 @@ resource "aws_dynamodb_table" "subscriptions-table" {
305305
Use the [`subscribe`](docs/README.md#subscribe) function to associate incoming subscriptions with a topic.
306306

307307
```ts
308-
import { subscribe } from 'graphql-lambda-subscriptions';
308+
import { subscribe } from 'graphql-lambda-subscriptions'
309309

310310
export const resolver = {
311311
Subscribe: {
@@ -326,7 +326,7 @@ Use the [`subscribe`](docs/README.md#subscribe) with [`SubscribeOptions`](docs/i
326326
> Note: If a function is provided, it will be called **on subscription start** and must return a serializable object.
327327
328328
```ts
329-
import { subscribe } from 'graphql-lambda-subscriptions';
329+
import { subscribe } from 'graphql-lambda-subscriptions'
330330

331331
// Subscription agnostic filter
332332
subscribe('MY_TOPIC', {
@@ -350,9 +350,9 @@ subscribe('MY_TOPIC',{
350350

351351
#### Publishing events
352352

353-
Use the `publish` on your graphql-lambda-subscriptions server to publish events to active subscriptions. Payloads must be of type `Record<string, any>` so they can be filtered and stored.
353+
Use the [`publish()`](docs/interfaces/SubscriptionServer.md#publish) function on your graphql-lambda-subscriptions server to publish events to active subscriptions. Payloads must be of type `Record<string, any>` so they can be filtered and stored.
354354

355-
```tsx
355+
```ts
356356
subscriptionServer.publish({
357357
type: 'MY_TOPIC',
358358
payload: {
@@ -363,7 +363,7 @@ subscriptionServer.publish({
363363

364364
Events can come from many sources
365365

366-
```tsx
366+
```ts
367367
// SNS Event
368368
export const snsHandler = (event) =>
369369
Promise.all(
@@ -373,17 +373,17 @@ export const snsHandler = (event) =>
373373
payload: JSON.parse(r.Sns.Message),
374374
})
375375
)
376-
);
376+
)
377377

378378
// Manual Invocation
379-
export const invocationHandler = (payload) => subscriptionServer.publish({ topic: 'MY_TOPIC', payload });
379+
export const invocationHandler = (payload) => subscriptionServer.publish({ topic: 'MY_TOPIC', payload })
380380
```
381381

382382
#### Completing Subscriptions
383383

384384
Use the `complete` on your graphql-lambda-subscriptions server to complete active subscriptions. Payloads are optional and match against filters like events do.
385385

386-
```tsx
386+
```ts
387387
subscriptionServer.complete({
388388
type: 'MY_TOPIC',
389389
// optional payload
@@ -395,33 +395,11 @@ subscriptionServer.complete({
395395

396396
### Context
397397

398-
Context values are accessible in all callback and resolver functions (`resolve`, `filter`, `onAfterSubscribe`, `onSubscribe` and `onComplete`).
399-
400-
<details>
401-
402-
<summary>📖 Default value</summary>
403-
404-
Assuming no `context` argument is provided, the default value is an object containing a `connectionInitPayload` attribute.
405-
406-
This attribute contains the [(optionally parsed)](#events) payload from `connection_init`.
407-
408-
```ts
409-
export const resolver = {
410-
Subscribe: {
411-
mySubscription: {
412-
resolve: (event, args, context) => {
413-
console.log(context.connectionInitPayload); // payload from connection_init
414-
},
415-
},
416-
},
417-
};
418-
```
419-
420-
</details>
398+
[Context](docs/interfaces/ServerArgs.md#context) is provided on the [`ServerArgs`](docs/interfaces/ServerArgs.md) object when creating a server. The values are accessible in all callback and resolver functions (eg. `resolve`, `filter`, `onAfterSubscribe`, `onSubscribe` and `onComplete`).
421399

422-
<details>
400+
Assuming no `context` argument is provided when creating the server, the default value is an object with `connectionInitPayload`, `connectionId` properties and the [`publish()`](docs/interfaces/SubscriptionServer.md#publish) and [`complete()`](docs/interfaces/SubscriptionServer.md#complete) functions. These properties are merged into a provided object or passed into a provided function.
423401

424-
<summary>📖 Setting static context value</summary>
402+
#### Setting static context value
425403

426404
An object can be provided via the `context` attribute when calling `makeServer`.
427405

@@ -431,16 +409,12 @@ const instance = makeServer({
431409
context: {
432410
myAttr: 'hello',
433411
},
434-
});
412+
})
435413
```
436414

437415
The default values (above) will be appended to this object prior to execution.
438416

439-
</details>
440-
441-
<details>
442-
443-
<summary>📖 Setting dynamic context value</summary>
417+
#### Setting dynamic context value
444418

445419
A function (optionally async) can be provided via the `context` attribute when calling `makeServer`.
446420

@@ -453,10 +427,31 @@ const instance = makeServer({
453427
myAttr: 'hello',
454428
user: connectionInitPayload.user,
455429
}),
456-
});
430+
})
457431
```
458432

459-
</details>
433+
#### Using the context
434+
435+
```ts
436+
export const resolver = {
437+
Subscribe: {
438+
mySubscription: {
439+
subscribe: subscribe('GREETINGS', {
440+
filter(_, _, context) {
441+
console.log(context.connectionId) // the connectionId
442+
},
443+
async onAfterSubscribe(_, _, { connectionId, publish }) {
444+
await publish('GREETINGS', { message: `HI from ${connectionId}!` })
445+
}
446+
})
447+
resolve: (event, args, context) => {
448+
console.log(context.connectionInitPayload) // payload from connection_init
449+
return event.payload.message
450+
},
451+
},
452+
},
453+
}
454+
```
460455

461456
### Side effects
462457

@@ -481,7 +476,7 @@ export const resolver = {
481476
}),
482477
},
483478
},
484-
};
479+
}
485480
```
486481

487482
</details>
@@ -502,7 +497,7 @@ const instance = makeServer({
502497
onConnect: ({ event }) => {
503498
/* */
504499
},
505-
});
500+
})
506501
```
507502

508503
</details>
@@ -519,7 +514,7 @@ const instance = makeServer({
519514
onDisconnect: ({ event }) => {
520515
/* */
521516
},
522-
});
517+
})
523518
```
524519

525520
</details>
@@ -536,19 +531,19 @@ const instance = makeServer({
536531
const instance = makeServer({
537532
/* ... */
538533
onConnectionInit: ({ message }) => {
539-
const token = message.payload.token;
534+
const token = message.payload.token
540535

541536
if (!myValidation(token)) {
542-
throw Error('Token validation failed');
537+
throw Error('Token validation failed')
543538
}
544539

545540
// Prevent sensitive data from being written to DB
546541
return {
547542
...message.payload,
548543
token: undefined,
549-
};
544+
}
550545
},
551-
});
546+
})
552547
```
553548

554549
By default, the (optionally parsed) payload will be accessible via [context](#context).
@@ -569,7 +564,7 @@ const instance = makeServer({
569564
onSubscribe: ({ event, message }) => {
570565
/* */
571566
},
572-
});
567+
})
573568
```
574569

575570
</details>
@@ -586,7 +581,7 @@ const instance = makeServer({
586581
onComplete: ({ event, message }) => {
587582
/* */
588583
},
589-
});
584+
})
590585
```
591586

592587
</details>
@@ -603,7 +598,7 @@ const instance = makeServer({
603598
onError: (error, context) => {
604599
/* */
605600
},
606-
});
601+
})
607602
```
608603

609604
</details>

docs/interfaces/ServerArgs.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ ___
3535

3636
### context
3737

38-
`Optional` **context**: `object` \| (`arg`: { `connectionId`: `string` ; `connectionInitPayload`: `any` }) => [`MaybePromise`](../README.md#maybepromise)<`object`\>
38+
`Optional` **context**: `object` \| (`arg`: { `complete`: (`event`: { `payload?`: `Record`<`string`, `any`\> ; `topic`: `string` }) => `Promise`<`void`\> ; `connectionId`: `string` ; `connectionInitPayload`: `any` ; `publish`: (`event`: { `payload`: `Record`<`string`, `any`\> ; `topic`: `string` }) => `Promise`<`void`\> }) => [`MaybePromise`](../README.md#maybepromise)<`object`\>
3939

4040
Makes the context object for all operations defaults to { connectionInitPayload, connectionId }
4141

@@ -59,7 +59,7 @@ ___
5959

6060
`Optional` **pingpong**: [`MaybePromise`](../README.md#maybepromise)<`Object`\>
6161

62-
If set
62+
If set you can use the `stepFunctionsHandler` and a step function to setup a per connection ping/pong cycle to detect disconnects sooner than the 10 minute idle timeout.
6363

6464
___
6565

lib/messages/complete.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { buildExecutionContext } from 'graphql/execution/execute'
55
import { collect } from 'streaming-iterables'
66
import { SubscribePseudoIterable, MessageHandler, PubSubEvent } from '../types'
77
import { deleteConnection } from '../utils/deleteConnection'
8-
import { constructContext } from '../utils/constructContext'
8+
import { buildContext } from '../utils/buildContext'
99
import { getResolverAndArgs } from '../utils/getResolverAndArgs'
1010
import { isArray } from '../utils/isArray'
1111

@@ -26,7 +26,7 @@ export const complete: MessageHandler<CompleteMessage> =
2626
server.schema,
2727
parse(sub.subscription.query),
2828
undefined,
29-
await constructContext({ server, connectionInitPayload: sub.connectionInitPayload, connectionId: sub.connectionId }),
29+
await buildContext({ server, connectionInitPayload: sub.connectionInitPayload, connectionId: sub.connectionId }),
3030
sub.subscription.variables,
3131
sub.subscription.operationName,
3232
undefined,

lib/messages/disconnect.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import AggregateError from 'aggregate-error'
22
import { parse } from 'graphql'
33
import { equals } from '@aws/dynamodb-expressions'
44
import { buildExecutionContext } from 'graphql/execution/execute'
5-
import { constructContext } from '../utils/constructContext'
5+
import { buildContext } from '../utils/buildContext'
66
import { getResolverAndArgs } from '../utils/getResolverAndArgs'
77
import { SubscribePseudoIterable, MessageHandler, PubSubEvent } from '../types'
88
import { isArray } from '../utils/isArray'
@@ -36,7 +36,7 @@ export const disconnect: MessageHandler<null> = async ({ server, event }) => {
3636
server.schema,
3737
parse(sub.subscription.query),
3838
undefined,
39-
await constructContext({ server, connectionInitPayload: sub.connectionInitPayload, connectionId: sub.connectionId }),
39+
await buildContext({ server, connectionInitPayload: sub.connectionInitPayload, connectionId: sub.connectionId }),
4040
sub.subscription.variables,
4141
sub.subscription.operationName,
4242
undefined,

lib/messages/subscribe.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
execute,
77
} from 'graphql/execution/execute'
88
import { APIGatewayWebSocketEvent, ServerClosure, MessageHandler, SubscribePseudoIterable, PubSubEvent } from '../types'
9-
import { constructContext } from '../utils/constructContext'
9+
import { buildContext } from '../utils/buildContext'
1010
import { getResolverAndArgs } from '../utils/getResolverAndArgs'
1111
import { postToConnection } from '../utils/postToConnection'
1212
import { deleteConnection } from '../utils/deleteConnection'
@@ -48,7 +48,7 @@ const setupSubscription: MessageHandler<SubscribeMessage> = async ({ server, eve
4848
})
4949
}
5050

51-
const contextValue = await constructContext({ server, connectionInitPayload: connection.payload, connectionId })
51+
const contextValue = await buildContext({ server, connectionInitPayload: connection.payload, connectionId })
5252

5353
const execContext = buildExecutionContext(
5454
server.schema,

lib/pubsub/complete.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { CompleteMessage, MessageType } from 'graphql-ws'
44
import { buildExecutionContext } from 'graphql/execution/execute'
55
import { ServerClosure, PubSubEvent, SubscribePseudoIterable, SubscriptionServer } from '../types'
66
import { postToConnection } from '../utils/postToConnection'
7-
import { constructContext } from '../utils/constructContext'
7+
import { buildContext } from '../utils/buildContext'
88
import { getResolverAndArgs } from '../utils/getResolverAndArgs'
99
import { isArray } from '../utils/isArray'
1010
import { getFilteredSubs } from './getFilteredSubs'
1111

12-
export const complete = (serverPromise: Promise<ServerClosure>): SubscriptionServer['complete'] => async event => {
12+
export const complete = (serverPromise: Promise<ServerClosure> | ServerClosure): SubscriptionServer['complete'] => async event => {
1313
const server = await serverPromise
1414
const subscriptions = await getFilteredSubs({ server, event })
1515
server.log('pubsub:complete %j', { event, subscriptions })
@@ -29,7 +29,7 @@ export const complete = (serverPromise: Promise<ServerClosure>): SubscriptionSer
2929
server.schema,
3030
parse(sub.subscription.query),
3131
undefined,
32-
await constructContext({ server, connectionInitPayload: sub.connectionInitPayload, connectionId: sub.connectionId }),
32+
await buildContext({ server, connectionInitPayload: sub.connectionInitPayload, connectionId: sub.connectionId }),
3333
sub.subscription.variables,
3434
sub.subscription.operationName,
3535
undefined,

lib/pubsub/publish.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { parse, execute } from 'graphql'
22
import { MessageType, NextMessage } from 'graphql-ws'
33
import { ServerClosure, SubscriptionServer } from '../types'
44
import { postToConnection } from '../utils/postToConnection'
5-
import { constructContext } from '../utils/constructContext'
5+
import { buildContext } from '../utils/buildContext'
66
import { getFilteredSubs } from './getFilteredSubs'
77

8-
export const publish = (serverPromise: Promise<ServerClosure>): SubscriptionServer['publish'] => async event => {
8+
export const publish = (serverPromise: Promise<ServerClosure> | ServerClosure): SubscriptionServer['publish'] => async event => {
99
const server = await serverPromise
1010
server.log('pubsub:publish %j', { event })
1111
const subscriptions = await getFilteredSubs({ server, event })
@@ -16,7 +16,7 @@ export const publish = (serverPromise: Promise<ServerClosure>): SubscriptionServ
1616
server.schema,
1717
parse(sub.subscription.query),
1818
event,
19-
await constructContext({ server, connectionInitPayload: sub.connectionInitPayload, connectionId: sub.connectionId }),
19+
await buildContext({ server, connectionInitPayload: sub.connectionInitPayload, connectionId: sub.connectionId }),
2020
sub.subscription.variables,
2121
sub.subscription.operationName,
2222
undefined,

lib/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ export interface ServerArgs {
3838
/**
3939
* Makes the context object for all operations defaults to { connectionInitPayload, connectionId }
4040
*/
41-
context?: ((arg: { connectionInitPayload: any, connectionId: string }) => MaybePromise<object>) | object
41+
context?: ((arg: { connectionInitPayload: any, connectionId: string, publish: SubscriptionServer['publish'], complete: SubscriptionServer['complete'] }) => MaybePromise<object>) | object
4242
/**
43-
* If set
43+
* If set you can use the `stepFunctionsHandler` and a step function to setup a per connection ping/pong cycle to detect disconnects sooner than the 10 minute idle timeout.
4444
*/
4545
pingpong?: MaybePromise<{
4646
/**

lib/utils/buildContext.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* eslint-disable @typescript-eslint/ban-types */
2+
import { complete as completeFactory } from '../pubsub/complete'
3+
import { publish as publishFactory } from '../pubsub/publish'
4+
import { ServerClosure } from '../types'
5+
6+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7+
export const buildContext = ({ server, connectionInitPayload, connectionId }: { connectionInitPayload: object, server: ServerClosure, connectionId: string }): any => {
8+
const publish = publishFactory(server)
9+
const complete = completeFactory(server)
10+
if (typeof server.context === 'function') {
11+
return server.context({ connectionInitPayload, connectionId, publish, complete })
12+
}
13+
return { ...server.context, connectionInitPayload, connectionId, publish, complete }
14+
}

0 commit comments

Comments
 (0)