Skip to content

Commit 45868ae

Browse files
wyattjohcvle
andauthored
fix: apply a fix to resolve memory leak from subscription promise leakage (#3241)
Co-authored-by: Vinh <[email protected]>
1 parent 3743f89 commit 45868ae

File tree

1 file changed

+58
-27
lines changed
  • src/core/server/graph/resolvers/Subscription

1 file changed

+58
-27
lines changed

src/core/server/graph/resolvers/Subscription/helpers.ts

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import { GraphQLResolveInfo } from "graphql";
2-
import { withFilter } from "graphql-subscriptions";
32

43
import GraphContext from "../../context";
54
import { SUBSCRIPTION_CHANNELS, SubscriptionPayload } from "./types";
65

6+
type ResolverFn<TParent, TArgs, TContext> = (
7+
parent: TParent,
8+
args: TArgs,
9+
context: TContext,
10+
info: GraphQLResolveInfo
11+
) => AsyncIterable<any>;
12+
713
type FilterFn<TParent, TArgs, TContext> = (
814
parent: TParent,
915
args: TArgs,
@@ -23,13 +29,51 @@ interface SubscriptionResolver<TParent, TArgs, TResult> {
2329
resolve: Resolver<TParent, TArgs, TParent>;
2430
}
2531

26-
export function createTenantAsyncIterator<TParent, TArgs, TResult>(
32+
/**
33+
* withFilter applies a filter to a async iterator.
34+
*
35+
* This duplicates the functionality of the withFilter function provided by the
36+
* `graphql-subscriptions` package without the memory leak as it uses native
37+
* async iterators instead.
38+
*
39+
* Solution provided by @brettjashford.
40+
*
41+
* https://github.com/apollographql/graphql-subscriptions/pull/209#issuecomment-713906710
42+
*
43+
* @param asyncIteratorFn the async iterator to use that's provided by the transport
44+
* @param filterFn the filter to apply for each iteration to check to see if we should sent it
45+
*/
46+
function withFilter<TParent, TArgs>(
47+
asyncIteratorFn: ResolverFn<TParent, TArgs, GraphContext>,
48+
filterFn: FilterFn<TParent, TArgs, GraphContext>
49+
) {
50+
return async function* (
51+
source: TParent,
52+
args: TArgs,
53+
ctx: GraphContext,
54+
info: GraphQLResolveInfo
55+
) {
56+
const asyncIterator = asyncIteratorFn(source, args, ctx, info);
57+
for await (const payload of asyncIterator) {
58+
if (await filterFn(payload, args, ctx, info)) {
59+
yield payload;
60+
}
61+
}
62+
};
63+
}
64+
65+
function createTenantAsyncIterator<TParent, TArgs, TResult>(
2766
channel: SUBSCRIPTION_CHANNELS
28-
): Resolver<TParent, TArgs, AsyncIterator<TResult>> {
67+
): Resolver<TParent, TArgs, AsyncIterable<TResult>> {
2968
return (source, args, ctx) =>
30-
ctx.pubsub.asyncIterator<TResult>(
69+
// This is already technically returning an AsyncIterable, the Typescript
70+
// types are in fact wrong:
71+
//
72+
// https://github.com/davidyaha/graphql-redis-subscriptions/pull/255
73+
//
74+
(ctx.pubsub.asyncIterator<TResult>(
3175
createSubscriptionChannelName(ctx.tenant.id, channel)
32-
);
76+
) as unknown) as AsyncIterable<TResult>;
3377
}
3478

3579
export function createSubscriptionChannelName(
@@ -40,7 +84,7 @@ export function createSubscriptionChannelName(
4084
}
4185

4286
/**
43-
* defaultFilterFn will perform filtering operations on the subscription
87+
* clientIDFilterFn will perform filtering operations on the subscription
4488
* responses to ensure that mutations issued by one user is not sent back as a
4589
* subscription to the same requesting User, as they already implement the
4690
* update via the mutation response.
@@ -53,7 +97,7 @@ export function createSubscriptionChannelName(
5397
* need to determine eligibility to send the subscription back or
5498
* not.
5599
*/
56-
export function defaultFilterFn<TParent extends SubscriptionPayload, TArgs>(
100+
export function clientIDFilterFn<TParent extends SubscriptionPayload, TArgs>(
57101
source: TParent,
58102
args: TArgs,
59103
ctx: GraphContext
@@ -65,28 +109,15 @@ export function defaultFilterFn<TParent extends SubscriptionPayload, TArgs>(
65109
return true;
66110
}
67111

68-
/**
69-
* Ensure that even when we're provided with a domain specific filtering
70-
* function we respect the subscription id that is sent back with the request to
71-
* prevent double responses.
72-
*/
73-
export function createFilterFn<TParent, TArgs>(
74-
filter?: FilterFn<TParent, TArgs, GraphContext>
112+
function composeFilters<TParent, TArgs>(
113+
...filters: Array<FilterFn<TParent, TArgs, GraphContext>>
75114
): FilterFn<TParent, TArgs, GraphContext> {
76-
return filter
77-
? // Combine the filters, preferring the defaultFilterFn first.
78-
(source, args, ctx, info) => {
79-
if (!defaultFilterFn(source, args, ctx)) {
80-
return false;
81-
}
82-
83-
return filter(source, args, ctx, info);
84-
}
85-
: defaultFilterFn;
115+
return (source, args, ctx, info) =>
116+
filters.every((filter) => filter(source, args, ctx, info));
86117
}
87118

88119
export interface CreateIteratorInput<TParent, TArgs, TResult> {
89-
filter?: FilterFn<TParent, TArgs, GraphContext>;
120+
filter: FilterFn<TParent, TArgs, GraphContext>;
90121
}
91122

92123
export function createIterator<
@@ -95,12 +126,12 @@ export function createIterator<
95126
TResult
96127
>(
97128
channel: SUBSCRIPTION_CHANNELS,
98-
{ filter }: CreateIteratorInput<TParent, TArgs, TResult> = {}
129+
{ filter }: CreateIteratorInput<TParent, TArgs, TResult>
99130
): SubscriptionResolver<TParent, TArgs, TResult> {
100131
return {
101132
subscribe: withFilter(
102133
createTenantAsyncIterator(channel),
103-
createFilterFn(filter)
134+
composeFilters(clientIDFilterFn, filter)
104135
),
105136
resolve: (payload) => payload,
106137
};

0 commit comments

Comments
 (0)