|
| 1 | +import { isPromise } from '../jsutils/isPromise.js'; |
1 | 2 | import type { ObjMap } from '../jsutils/ObjMap.js';
|
2 | 3 | import type { Path } from '../jsutils/Path.js';
|
3 | 4 | import { pathToArray } from '../jsutils/Path.js';
|
| 5 | +import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js'; |
4 | 6 | import { promiseWithResolvers } from '../jsutils/promiseWithResolvers.js';
|
5 | 7 |
|
6 | 8 | import type {
|
7 | 9 | GraphQLError,
|
8 | 10 | GraphQLFormattedError,
|
9 | 11 | } from '../error/GraphQLError.js';
|
10 | 12 |
|
| 13 | +/** |
| 14 | + * The result of GraphQL execution. |
| 15 | + * |
| 16 | + * - `errors` is included when any errors occurred as a non-empty array. |
| 17 | + * - `data` is the result of a successful execution of the query. |
| 18 | + * - `hasNext` is true if a future payload is expected. |
| 19 | + * - `extensions` is reserved for adding non-standard properties. |
| 20 | + * - `incremental` is a list of the results from defer/stream directives. |
| 21 | + */ |
| 22 | +export interface ExecutionResult< |
| 23 | + TData = ObjMap<unknown>, |
| 24 | + TExtensions = ObjMap<unknown>, |
| 25 | +> { |
| 26 | + errors?: ReadonlyArray<GraphQLError>; |
| 27 | + data?: TData | null; |
| 28 | + extensions?: TExtensions; |
| 29 | +} |
| 30 | + |
| 31 | +export interface FormattedExecutionResult< |
| 32 | + TData = ObjMap<unknown>, |
| 33 | + TExtensions = ObjMap<unknown>, |
| 34 | +> { |
| 35 | + errors?: ReadonlyArray<GraphQLFormattedError>; |
| 36 | + data?: TData | null; |
| 37 | + extensions?: TExtensions; |
| 38 | +} |
| 39 | + |
| 40 | +export interface ExperimentalIncrementalExecutionResults< |
| 41 | + TData = ObjMap<unknown>, |
| 42 | + TExtensions = ObjMap<unknown>, |
| 43 | +> { |
| 44 | + initialResult: InitialIncrementalExecutionResult<TData, TExtensions>; |
| 45 | + subsequentResults: AsyncGenerator< |
| 46 | + SubsequentIncrementalExecutionResult<TData, TExtensions>, |
| 47 | + void, |
| 48 | + void |
| 49 | + >; |
| 50 | +} |
| 51 | + |
| 52 | +export interface InitialIncrementalExecutionResult< |
| 53 | + TData = ObjMap<unknown>, |
| 54 | + TExtensions = ObjMap<unknown>, |
| 55 | +> extends ExecutionResult<TData, TExtensions> { |
| 56 | + hasNext: boolean; |
| 57 | + incremental?: ReadonlyArray<IncrementalResult<TData, TExtensions>>; |
| 58 | + extensions?: TExtensions; |
| 59 | +} |
| 60 | + |
| 61 | +export interface FormattedInitialIncrementalExecutionResult< |
| 62 | + TData = ObjMap<unknown>, |
| 63 | + TExtensions = ObjMap<unknown>, |
| 64 | +> extends FormattedExecutionResult<TData, TExtensions> { |
| 65 | + hasNext: boolean; |
| 66 | + incremental?: ReadonlyArray<FormattedIncrementalResult<TData, TExtensions>>; |
| 67 | + extensions?: TExtensions; |
| 68 | +} |
| 69 | + |
11 | 70 | export interface SubsequentIncrementalExecutionResult<
|
12 | 71 | TData = ObjMap<unknown>,
|
13 | 72 | TExtensions = ObjMap<unknown>,
|
@@ -113,86 +172,6 @@ export class IncrementalPublisher {
|
113 | 172 | this._reset();
|
114 | 173 | }
|
115 | 174 |
|
116 |
| - hasNext(): boolean { |
117 |
| - return this._pending.size > 0; |
118 |
| - } |
119 |
| - |
120 |
| - subscribe(): AsyncGenerator< |
121 |
| - SubsequentIncrementalExecutionResult, |
122 |
| - void, |
123 |
| - void |
124 |
| - > { |
125 |
| - let isDone = false; |
126 |
| - |
127 |
| - const _next = async (): Promise< |
128 |
| - IteratorResult<SubsequentIncrementalExecutionResult, void> |
129 |
| - > => { |
130 |
| - // eslint-disable-next-line no-constant-condition |
131 |
| - while (true) { |
132 |
| - if (isDone) { |
133 |
| - return { value: undefined, done: true }; |
134 |
| - } |
135 |
| - |
136 |
| - for (const item of this._released) { |
137 |
| - this._pending.delete(item); |
138 |
| - } |
139 |
| - const released = this._released; |
140 |
| - this._released = new Set(); |
141 |
| - |
142 |
| - const result = this._getIncrementalResult(released); |
143 |
| - |
144 |
| - if (!this.hasNext()) { |
145 |
| - isDone = true; |
146 |
| - } |
147 |
| - |
148 |
| - if (result !== undefined) { |
149 |
| - return { value: result, done: false }; |
150 |
| - } |
151 |
| - |
152 |
| - // eslint-disable-next-line no-await-in-loop |
153 |
| - await this._signalled; |
154 |
| - } |
155 |
| - }; |
156 |
| - |
157 |
| - const returnStreamIterators = async (): Promise<void> => { |
158 |
| - const promises: Array<Promise<IteratorResult<unknown>>> = []; |
159 |
| - this._pending.forEach((incrementalDataRecord) => { |
160 |
| - if ( |
161 |
| - isStreamItemsRecord(incrementalDataRecord) && |
162 |
| - incrementalDataRecord.asyncIterator?.return |
163 |
| - ) { |
164 |
| - promises.push(incrementalDataRecord.asyncIterator.return()); |
165 |
| - } |
166 |
| - }); |
167 |
| - await Promise.all(promises); |
168 |
| - }; |
169 |
| - |
170 |
| - const _return = async (): Promise< |
171 |
| - IteratorResult<SubsequentIncrementalExecutionResult, void> |
172 |
| - > => { |
173 |
| - isDone = true; |
174 |
| - await returnStreamIterators(); |
175 |
| - return { value: undefined, done: true }; |
176 |
| - }; |
177 |
| - |
178 |
| - const _throw = async ( |
179 |
| - error?: unknown, |
180 |
| - ): Promise<IteratorResult<SubsequentIncrementalExecutionResult, void>> => { |
181 |
| - isDone = true; |
182 |
| - await returnStreamIterators(); |
183 |
| - return Promise.reject(error); |
184 |
| - }; |
185 |
| - |
186 |
| - return { |
187 |
| - [Symbol.asyncIterator]() { |
188 |
| - return this; |
189 |
| - }, |
190 |
| - next: _next, |
191 |
| - return: _return, |
192 |
| - throw: _throw, |
193 |
| - }; |
194 |
| - } |
195 |
| - |
196 | 175 | prepareInitialResultRecord(): InitialResultRecord {
|
197 | 176 | return {
|
198 | 177 | errors: [],
|
@@ -256,19 +235,26 @@ export class IncrementalPublisher {
|
256 | 235 | incrementalDataRecord.errors.push(error);
|
257 | 236 | }
|
258 | 237 |
|
259 |
| - publishInitial(initialResult: InitialResultRecord) { |
260 |
| - for (const child of initialResult.children) { |
261 |
| - if (child.filtered) { |
262 |
| - continue; |
263 |
| - } |
264 |
| - this._publish(child); |
| 238 | + handleInitialResultData( |
| 239 | + initialResultRecord: InitialResultRecord, |
| 240 | + data: PromiseOrValue<ObjMap<unknown>>, |
| 241 | + ): PromiseOrValue<ExecutionResult | ExperimentalIncrementalExecutionResults> { |
| 242 | + if (isPromise(data)) { |
| 243 | + return data.then( |
| 244 | + (resolved) => this._buildInitialResponse(initialResultRecord, resolved), |
| 245 | + (error) => this.handleInitialResultError(initialResultRecord, error), |
| 246 | + ); |
265 | 247 | }
|
| 248 | + return this._buildInitialResponse(initialResultRecord, data); |
266 | 249 | }
|
267 | 250 |
|
268 |
| - getInitialErrors( |
269 |
| - initialResult: InitialResultRecord, |
270 |
| - ): ReadonlyArray<GraphQLError> { |
271 |
| - return initialResult.errors; |
| 251 | + handleInitialResultError( |
| 252 | + initialResultRecord: InitialResultRecord, |
| 253 | + error: GraphQLError, |
| 254 | + ): ExecutionResult { |
| 255 | + const errors = initialResultRecord.errors; |
| 256 | + errors.push(error); |
| 257 | + return { data: null, errors }; |
272 | 258 | }
|
273 | 259 |
|
274 | 260 | filter(nullPath: Path, erroringIncrementalDataRecord: IncrementalDataRecord) {
|
@@ -302,6 +288,111 @@ export class IncrementalPublisher {
|
302 | 288 | });
|
303 | 289 | }
|
304 | 290 |
|
| 291 | + private _buildInitialResponse( |
| 292 | + initialResultRecord: InitialResultRecord, |
| 293 | + data: ObjMap<unknown> | null, |
| 294 | + ): ExecutionResult | ExperimentalIncrementalExecutionResults { |
| 295 | + for (const child of initialResultRecord.children) { |
| 296 | + if (child.filtered) { |
| 297 | + continue; |
| 298 | + } |
| 299 | + this._publish(child); |
| 300 | + } |
| 301 | + |
| 302 | + const errors = initialResultRecord.errors; |
| 303 | + const initialResult = errors.length === 0 ? { data } : { errors, data }; |
| 304 | + if (this._hasNext()) { |
| 305 | + return { |
| 306 | + initialResult: { |
| 307 | + ...initialResult, |
| 308 | + hasNext: true, |
| 309 | + }, |
| 310 | + subsequentResults: this._subscribe(), |
| 311 | + }; |
| 312 | + } |
| 313 | + return initialResult; |
| 314 | + } |
| 315 | + |
| 316 | + private _hasNext(): boolean { |
| 317 | + return this._pending.size > 0; |
| 318 | + } |
| 319 | + |
| 320 | + private _subscribe(): AsyncGenerator< |
| 321 | + SubsequentIncrementalExecutionResult, |
| 322 | + void, |
| 323 | + void |
| 324 | + > { |
| 325 | + let isDone = false; |
| 326 | + |
| 327 | + const _next = async (): Promise< |
| 328 | + IteratorResult<SubsequentIncrementalExecutionResult, void> |
| 329 | + > => { |
| 330 | + // eslint-disable-next-line no-constant-condition |
| 331 | + while (true) { |
| 332 | + if (isDone) { |
| 333 | + return { value: undefined, done: true }; |
| 334 | + } |
| 335 | + |
| 336 | + for (const item of this._released) { |
| 337 | + this._pending.delete(item); |
| 338 | + } |
| 339 | + const released = this._released; |
| 340 | + this._released = new Set(); |
| 341 | + |
| 342 | + const result = this._getIncrementalResult(released); |
| 343 | + |
| 344 | + if (!this._hasNext()) { |
| 345 | + isDone = true; |
| 346 | + } |
| 347 | + |
| 348 | + if (result !== undefined) { |
| 349 | + return { value: result, done: false }; |
| 350 | + } |
| 351 | + |
| 352 | + // eslint-disable-next-line no-await-in-loop |
| 353 | + await this._signalled; |
| 354 | + } |
| 355 | + }; |
| 356 | + |
| 357 | + const returnStreamIterators = async (): Promise<void> => { |
| 358 | + const promises: Array<Promise<IteratorResult<unknown>>> = []; |
| 359 | + this._pending.forEach((incrementalDataRecord) => { |
| 360 | + if ( |
| 361 | + isStreamItemsRecord(incrementalDataRecord) && |
| 362 | + incrementalDataRecord.asyncIterator?.return |
| 363 | + ) { |
| 364 | + promises.push(incrementalDataRecord.asyncIterator.return()); |
| 365 | + } |
| 366 | + }); |
| 367 | + await Promise.all(promises); |
| 368 | + }; |
| 369 | + |
| 370 | + const _return = async (): Promise< |
| 371 | + IteratorResult<SubsequentIncrementalExecutionResult, void> |
| 372 | + > => { |
| 373 | + isDone = true; |
| 374 | + await returnStreamIterators(); |
| 375 | + return { value: undefined, done: true }; |
| 376 | + }; |
| 377 | + |
| 378 | + const _throw = async ( |
| 379 | + error?: unknown, |
| 380 | + ): Promise<IteratorResult<SubsequentIncrementalExecutionResult, void>> => { |
| 381 | + isDone = true; |
| 382 | + await returnStreamIterators(); |
| 383 | + return Promise.reject(error); |
| 384 | + }; |
| 385 | + |
| 386 | + return { |
| 387 | + [Symbol.asyncIterator]() { |
| 388 | + return this; |
| 389 | + }, |
| 390 | + next: _next, |
| 391 | + return: _return, |
| 392 | + throw: _throw, |
| 393 | + }; |
| 394 | + } |
| 395 | + |
305 | 396 | private _trigger() {
|
306 | 397 | this._resolve();
|
307 | 398 | this._reset();
|
@@ -376,8 +467,8 @@ export class IncrementalPublisher {
|
376 | 467 | }
|
377 | 468 |
|
378 | 469 | return incrementalResults.length
|
379 |
| - ? { incremental: incrementalResults, hasNext: this.hasNext() } |
380 |
| - : encounteredCompletedAsyncIterator && !this.hasNext() |
| 470 | + ? { incremental: incrementalResults, hasNext: this._hasNext() } |
| 471 | + : encounteredCompletedAsyncIterator && !this._hasNext() |
381 | 472 | ? { hasNext: false }
|
382 | 473 | : undefined;
|
383 | 474 | }
|
|
0 commit comments