|
1 |
| -// |
2 |
| -// This is basically just event emitters wrapped with a function that filters messages. |
3 |
| -// |
4 | 1 | import { EventEmitter } from 'events';
|
5 |
| -import { |
6 |
| - GraphQLSchema, |
7 |
| - GraphQLError, |
8 |
| - validate, |
9 |
| - execute, |
10 |
| - parse, |
11 |
| - specifiedRules, |
12 |
| - OperationDefinitionNode, |
13 |
| - FieldNode, |
14 |
| -} from 'graphql'; |
15 |
| -import { getArgumentValues } from 'graphql/execution/values'; |
16 |
| - |
17 |
| -import { |
18 |
| - subscriptionHasSingleRootField, |
19 |
| -} from './validation'; |
20 |
| - |
21 |
| -export interface PubSubEngine { |
22 |
| - publish(triggerName: string, payload: any): boolean; |
23 |
| - subscribe(triggerName: string, onMessage: Function, options: Object): Promise<number>; |
24 |
| - unsubscribe(subId: number); |
25 |
| -} |
| 2 | +import { PubSubEngine } from './pubsub-engine'; |
| 3 | +import { eventEmitterAsyncIterator } from './event-emitter-to-async-iterator'; |
26 | 4 |
|
27 | 5 | export class PubSub implements PubSubEngine {
|
28 |
| - private ee: EventEmitter; |
29 |
| - private subscriptions: {[key: string]: [string, Function]}; |
30 |
| - private subIdCounter: number; |
31 |
| - |
32 |
| - constructor() { |
33 |
| - this.ee = new EventEmitter(); // max listeners = 10. |
34 |
| - this.subscriptions = {}; |
35 |
| - this.subIdCounter = 0; |
36 |
| - } |
37 |
| - |
38 |
| - public publish(triggerName: string, payload: any): boolean { |
39 |
| - this.ee.emit(triggerName, payload); |
40 |
| - // Not using the value returned from emit method because it gives |
41 |
| - // irrelevant false when there are no listeners. |
42 |
| - return true; |
43 |
| - } |
44 |
| - |
45 |
| - public subscribe(triggerName: string, onMessage: Function): Promise<number> { |
46 |
| - this.ee.addListener(triggerName, onMessage); |
47 |
| - this.subIdCounter = this.subIdCounter + 1; |
48 |
| - this.subscriptions[this.subIdCounter] = [triggerName, onMessage]; |
49 |
| - return Promise.resolve(this.subIdCounter); |
50 |
| - } |
51 |
| - |
52 |
| - public unsubscribe(subId: number) { |
53 |
| - const [triggerName, onMessage] = this.subscriptions[subId]; |
54 |
| - delete this.subscriptions[subId]; |
55 |
| - this.ee.removeListener(triggerName, onMessage); |
56 |
| - } |
57 |
| -} |
58 |
| - |
59 |
| -export class ValidationError extends Error { |
60 |
| - errors: Array<GraphQLError>; |
61 |
| - message: string; |
62 |
| - |
63 |
| - constructor(errors){ |
64 |
| - super(); |
65 |
| - this.errors = errors; |
66 |
| - this.message = 'Subscription query has validation errors'; |
67 |
| - } |
68 |
| -} |
69 |
| - |
70 |
| -export interface SubscriptionOptions { |
71 |
| - query: string; |
72 |
| - operationName: string; |
73 |
| - callback: Function; |
74 |
| - variables?: { [key: string]: any }; |
75 |
| - context?: any; |
76 |
| - formatError?: Function; |
77 |
| - formatResponse?: Function; |
78 |
| -}; |
79 |
| - |
80 |
| -export interface TriggerConfig { |
81 |
| - channelOptions?: Object; |
82 |
| - filter?: Function; |
83 |
| -} |
84 |
| - |
85 |
| -export interface TriggerMap { |
86 |
| - [triggerName: string]: TriggerConfig; |
87 |
| -} |
88 |
| - |
89 |
| -export interface SetupFunction { |
90 |
| - (options: SubscriptionOptions, args: {[key: string]: any}, subscriptionName: string): TriggerMap; |
91 |
| -} |
92 |
| - |
93 |
| -export interface SetupFunctions { |
94 |
| - [subscriptionName: string]: SetupFunction; |
95 |
| -} |
96 |
| - |
97 |
| -// This manages actual GraphQL subscriptions. |
98 |
| -export class SubscriptionManager { |
99 |
| - private pubsub: PubSubEngine; |
100 |
| - private schema: GraphQLSchema; |
101 |
| - private setupFunctions: SetupFunctions; |
102 |
| - private subscriptions: { [externalId: number]: Array<number>}; |
103 |
| - private maxSubscriptionId: number; |
104 |
| - |
105 |
| - constructor(options: { schema: GraphQLSchema, |
106 |
| - setupFunctions: SetupFunctions, |
107 |
| - pubsub: PubSubEngine }){ |
108 |
| - this.pubsub = options.pubsub; |
109 |
| - this.schema = options.schema; |
110 |
| - this.setupFunctions = options.setupFunctions || {}; |
111 |
| - this.subscriptions = {}; |
112 |
| - this.maxSubscriptionId = 0; |
113 |
| - } |
114 |
| - |
115 |
| - public publish(triggerName: string, payload: any) { |
116 |
| - this.pubsub.publish(triggerName, payload); |
117 |
| - } |
118 |
| - |
119 |
| - public subscribe(options: SubscriptionOptions): Promise<number> { |
120 |
| - |
121 |
| - // 1. validate the query, operationName and variables |
122 |
| - const parsedQuery = parse(options.query); |
123 |
| - const errors = validate( |
124 |
| - this.schema, |
125 |
| - parsedQuery, |
126 |
| - [...specifiedRules, subscriptionHasSingleRootField] |
127 |
| - ); |
128 |
| - |
129 |
| - // TODO: validate that all variables have been passed (and are of correct type)? |
130 |
| - if (errors.length){ |
131 |
| - // this error kills the subscription, so we throw it. |
132 |
| - return Promise.reject<number>(new ValidationError(errors)); |
133 |
| - } |
134 |
| - |
135 |
| - let args = {}; |
136 |
| - |
137 |
| - // operationName is the name of the only root field in the subscription document |
138 |
| - let subscriptionName = ''; |
139 |
| - parsedQuery.definitions.forEach( definition => { |
140 |
| - if (definition.kind === 'OperationDefinition'){ |
141 |
| - // only one root field is allowed on subscription. No fragments for now. |
142 |
| - const rootField = (definition as OperationDefinitionNode).selectionSet.selections[0] as FieldNode; |
143 |
| - subscriptionName = rootField.name.value; |
144 |
| - |
145 |
| - const fields = this.schema.getSubscriptionType().getFields(); |
146 |
| - args = getArgumentValues(fields[subscriptionName], rootField, options.variables); |
147 |
| - } |
148 |
| - }); |
149 |
| - |
150 |
| - let triggerMap: TriggerMap; |
151 |
| - |
152 |
| - if (this.setupFunctions[subscriptionName]) { |
153 |
| - triggerMap = this.setupFunctions[subscriptionName](options, args, subscriptionName); |
154 |
| - } else { |
155 |
| - // if not provided, the triggerName will be the subscriptionName, The trigger will not have any |
156 |
| - // options and rely on defaults that are set later. |
157 |
| - triggerMap = {[subscriptionName]: {}}; |
158 |
| - } |
159 |
| - |
160 |
| - const externalSubscriptionId = this.maxSubscriptionId++; |
161 |
| - this.subscriptions[externalSubscriptionId] = []; |
162 |
| - const subscriptionPromises = []; |
163 |
| - Object.keys(triggerMap).forEach( triggerName => { |
164 |
| - // Deconstruct the trigger options and set any defaults |
165 |
| - const { |
166 |
| - channelOptions = {}, |
167 |
| - filter = () => true, // Let all messages through by default. |
168 |
| - } = triggerMap[triggerName]; |
169 |
| - |
170 |
| - // 2. generate the handler function |
171 |
| - // |
172 |
| - // rootValue is the payload sent by the event emitter / trigger by |
173 |
| - // convention this is the value returned from the mutation |
174 |
| - // resolver |
175 |
| - const onMessage = (rootValue) => { |
176 |
| - return Promise.resolve().then(() => { |
177 |
| - if (typeof options.context === 'function') { |
178 |
| - return options.context(); |
179 |
| - } |
180 |
| - return options.context; |
181 |
| - }).then((context) => { |
182 |
| - return Promise.all([ |
183 |
| - context, |
184 |
| - filter(rootValue, context), |
185 |
| - ]); |
186 |
| - }).then(([context, doExecute]) => { |
187 |
| - if (!doExecute) { |
188 |
| - return; |
189 |
| - } |
190 |
| - execute( |
191 |
| - this.schema, |
192 |
| - parsedQuery, |
193 |
| - rootValue, |
194 |
| - context, |
195 |
| - options.variables, |
196 |
| - options.operationName |
197 |
| - ).then( data => options.callback(null, data) ); |
198 |
| - }).catch((error) => { |
199 |
| - options.callback(error); |
200 |
| - }); |
201 |
| - } |
202 |
| - |
203 |
| - // 3. subscribe and keep the subscription id |
204 |
| - subscriptionPromises.push( |
205 |
| - this.pubsub.subscribe(triggerName, onMessage, channelOptions) |
206 |
| - .then(id => this.subscriptions[externalSubscriptionId].push(id)) |
207 |
| - ); |
208 |
| - }); |
209 |
| - |
210 |
| - // Resolve the promise with external sub id only after all subscriptions completed |
211 |
| - return Promise.all(subscriptionPromises).then(() => externalSubscriptionId); |
212 |
| - } |
213 |
| - |
214 |
| - public unsubscribe(subId){ |
215 |
| - // pass the subId right through to pubsub. Do nothing else. |
216 |
| - this.subscriptions[subId].forEach( internalId => { |
217 |
| - this.pubsub.unsubscribe(internalId); |
218 |
| - }); |
219 |
| - delete this.subscriptions[subId]; |
220 |
| - } |
| 6 | + protected ee: EventEmitter; |
| 7 | + private subscriptions: { [key: string]: [string, Function] }; |
| 8 | + private subIdCounter: number; |
| 9 | + |
| 10 | + constructor() { |
| 11 | + this.ee = new EventEmitter(); |
| 12 | + this.subscriptions = {}; |
| 13 | + this.subIdCounter = 0; |
| 14 | + } |
| 15 | + |
| 16 | + public publish(triggerName: string, payload: any): boolean { |
| 17 | + this.ee.emit(triggerName, payload); |
| 18 | + |
| 19 | + return true; |
| 20 | + } |
| 21 | + |
| 22 | + public subscribe(triggerName: string, onMessage: Function): Promise<number> { |
| 23 | + this.ee.addListener(triggerName, onMessage); |
| 24 | + this.subIdCounter = this.subIdCounter + 1; |
| 25 | + this.subscriptions[this.subIdCounter] = [triggerName, onMessage]; |
| 26 | + |
| 27 | + return Promise.resolve(this.subIdCounter); |
| 28 | + } |
| 29 | + |
| 30 | + public unsubscribe(subId: number) { |
| 31 | + const [triggerName, onMessage] = this.subscriptions[subId]; |
| 32 | + delete this.subscriptions[subId]; |
| 33 | + this.ee.removeListener(triggerName, onMessage); |
| 34 | + } |
| 35 | + |
| 36 | + public asyncIterator<T>(triggers: string | string[]): AsyncIterator<T> { |
| 37 | + return eventEmitterAsyncIterator<T>(this.ee, triggers); |
| 38 | + } |
221 | 39 | }
|
0 commit comments