From 65a5ced96977a119be3dec9b1971fc8ab962fbd7 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 15 May 2023 12:57:58 +0300 Subject: [PATCH] add pending notifications --- src/execution/IncrementalPublisher.ts | 56 +++++++++++++++- src/execution/__tests__/defer-test.ts | 82 +++++++++++++++++++++++ src/execution/__tests__/mutations-test.ts | 2 + src/execution/__tests__/stream-test.ts | 52 ++++++++++++++ 4 files changed, 190 insertions(+), 2 deletions(-) diff --git a/src/execution/IncrementalPublisher.ts b/src/execution/IncrementalPublisher.ts index 8e40c908197..9dc4bc957a0 100644 --- a/src/execution/IncrementalPublisher.ts +++ b/src/execution/IncrementalPublisher.ts @@ -11,6 +11,7 @@ import type { import type { GroupedFieldSet } from './collectFields.js'; interface IncrementalUpdate> { + pending: ReadonlyArray; incremental: ReadonlyArray>; completed: ReadonlyArray; } @@ -59,6 +60,7 @@ export interface InitialIncrementalExecutionResult< TExtensions = ObjMap, > extends ExecutionResult { data: TData; + pending: ReadonlyArray; hasNext: true; extensions?: TExtensions; } @@ -68,6 +70,7 @@ export interface FormattedInitialIncrementalExecutionResult< TExtensions = ObjMap, > extends FormattedExecutionResult { data: TData; + pending: ReadonlyArray; hasNext: boolean; extensions?: TExtensions; } @@ -85,6 +88,7 @@ export interface FormattedSubsequentIncrementalExecutionResult< TExtensions = ObjMap, > { hasNext: boolean; + pending?: ReadonlyArray; incremental?: ReadonlyArray>; completed?: ReadonlyArray; extensions?: TExtensions; @@ -141,6 +145,11 @@ export type FormattedIncrementalResult< | FormattedIncrementalDeferResult | FormattedIncrementalStreamResult; +export interface PendingResult { + path: ReadonlyArray; + label?: string; +} + export interface CompletedResult { path: ReadonlyArray; label?: string; @@ -296,10 +305,20 @@ export class IncrementalPublisher { const errors = initialResultRecord.errors; const initialResult = errors.length === 0 ? { data } : { errors, data }; - if (this._pending.size > 0) { + const pending = this._pending; + if (pending.size > 0) { + const pendingSources = new Set(); + for (const subsequentResultRecord of pending) { + const pendingSource = isStreamItemsRecord(subsequentResultRecord) + ? subsequentResultRecord.streamRecord + : subsequentResultRecord; + pendingSources.add(pendingSource); + } + return { initialResult: { ...initialResult, + pending: this.pendingSourcesToResults(pendingSources), hasNext: true, }, subsequentResults: this._subscribe(), @@ -347,6 +366,23 @@ export class IncrementalPublisher { }); } + pendingSourcesToResults( + pendingSources: ReadonlySet, + ): Array { + const pendingResults: Array = []; + for (const pendingSource of pendingSources) { + pendingSource.pendingSent = true; + const pendingResult: PendingResult = { + path: pendingSource.path, + }; + if (pendingSource.label !== undefined) { + pendingResult.label = pendingSource.label; + } + pendingResults.push(pendingResult); + } + return pendingResults; + } + private _subscribe(): AsyncGenerator< SubsequentIncrementalExecutionResult, void, @@ -461,7 +497,8 @@ export class IncrementalPublisher { private _getIncrementalResult( completedRecords: ReadonlySet, ): SubsequentIncrementalExecutionResult | undefined { - const { incremental, completed } = this._processPending(completedRecords); + const { pending, incremental, completed } = + this._processPending(completedRecords); const hasNext = this._pending.size > 0; if (incremental.length === 0 && completed.length === 0 && hasNext) { @@ -469,6 +506,9 @@ export class IncrementalPublisher { } const result: SubsequentIncrementalExecutionResult = { hasNext }; + if (pending.length) { + result.pending = pending; + } if (incremental.length) { result.incremental = incremental; } @@ -482,6 +522,7 @@ export class IncrementalPublisher { private _processPending( completedRecords: ReadonlySet, ): IncrementalUpdate { + const newPendingSources = new Set(); const incrementalResults: Array = []; const completedResults: Array = []; for (const subsequentResultRecord of completedRecords) { @@ -489,10 +530,17 @@ export class IncrementalPublisher { if (child.filtered) { continue; } + const pendingSource = isStreamItemsRecord(child) + ? child.streamRecord + : child; + if (!pendingSource.pendingSent) { + newPendingSources.add(pendingSource); + } this._publish(child); } if (isStreamItemsRecord(subsequentResultRecord)) { if (subsequentResultRecord.isFinalRecord) { + newPendingSources.delete(subsequentResultRecord.streamRecord); completedResults.push( this._completedRecordToResult(subsequentResultRecord.streamRecord), ); @@ -513,6 +561,7 @@ export class IncrementalPublisher { } incrementalResults.push(incrementalResult); } else { + newPendingSources.delete(subsequentResultRecord); completedResults.push( this._completedRecordToResult(subsequentResultRecord), ); @@ -537,6 +586,7 @@ export class IncrementalPublisher { } return { + pending: this.pendingSourcesToResults(newPendingSources), incremental: incrementalResults, completed: completedResults, }; @@ -690,6 +740,7 @@ export class DeferredFragmentRecord { deferredGroupedFieldSetRecords: Set; errors: Array; filtered: boolean; + pendingSent?: boolean; _pending: Set; constructor(opts: { path: Path | undefined; label: string | undefined }) { @@ -709,6 +760,7 @@ export class StreamRecord { path: ReadonlyArray; errors: Array; earlyReturn?: (() => Promise) | undefined; + pendingSent?: boolean; constructor(opts: { label: string | undefined; path: Path; diff --git a/src/execution/__tests__/defer-test.ts b/src/execution/__tests__/defer-test.ts index a849e2722fb..68a56ee5054 100644 --- a/src/execution/__tests__/defer-test.ts +++ b/src/execution/__tests__/defer-test.ts @@ -186,6 +186,7 @@ describe('Execute: defer directive', () => { id: '1', }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -241,6 +242,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { id: '1' } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -271,6 +273,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: {}, + pending: [{ path: [], label: 'DeferQuery' }], hasNext: true, }, { @@ -312,6 +315,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: {}, + pending: [{ path: [], label: 'DeferQuery' }], hasNext: true, }, { @@ -361,6 +365,10 @@ describe('Execute: defer directive', () => { data: { hero: {}, }, + pending: [ + { path: ['hero'], label: 'DeferTop' }, + { path: ['hero'], label: 'DeferNested' }, + ], hasNext: true, }, { @@ -406,6 +414,7 @@ describe('Execute: defer directive', () => { name: 'Luke', }, }, + pending: [{ path: ['hero'], label: 'DeferTop' }], hasNext: true, }, { @@ -434,6 +443,7 @@ describe('Execute: defer directive', () => { name: 'Luke', }, }, + pending: [{ path: ['hero'], label: 'DeferTop' }], hasNext: true, }, { @@ -459,6 +469,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { id: '1' } }, + pending: [{ path: ['hero'], label: 'InlineDeferred' }], hasNext: true, }, { @@ -488,6 +499,7 @@ describe('Execute: defer directive', () => { data: { hero: {}, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -516,6 +528,10 @@ describe('Execute: defer directive', () => { data: { hero: {}, }, + pending: [ + { path: ['hero'], label: 'DeferID' }, + { path: ['hero'], label: 'DeferName' }, + ], hasNext: true, }, { @@ -561,6 +577,10 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: {}, + pending: [ + { path: [], label: 'DeferID' }, + { path: [], label: 'DeferName' }, + ], hasNext: true, }, { @@ -611,6 +631,10 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: {}, + pending: [ + { path: [], label: 'DeferID' }, + { path: [], label: 'DeferName' }, + ], hasNext: true, }, { @@ -658,6 +682,10 @@ describe('Execute: defer directive', () => { data: { hero: {}, }, + pending: [ + { path: [], label: 'DeferName' }, + { path: ['hero'], label: 'DeferID' }, + ], hasNext: true, }, { @@ -701,9 +729,11 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: {}, + pending: [{ path: [], label: 'DeferName' }], hasNext: true, }, { + pending: [{ path: ['hero'], label: 'DeferID' }], incremental: [ { data: { @@ -763,6 +793,20 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { friends: [{}, {}, {}] } }, + pending: [ + { path: ['hero', 'friends', 0] }, + { path: ['hero', 'friends', 0] }, + { path: ['hero', 'friends', 0] }, + { path: ['hero', 'friends', 0] }, + { path: ['hero', 'friends', 1] }, + { path: ['hero', 'friends', 1] }, + { path: ['hero', 'friends', 1] }, + { path: ['hero', 'friends', 1] }, + { path: ['hero', 'friends', 2] }, + { path: ['hero', 'friends', 2] }, + { path: ['hero', 'friends', 2] }, + { path: ['hero', 'friends', 2] }, + ], hasNext: true, }, { @@ -836,6 +880,7 @@ describe('Execute: defer directive', () => { }, }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -875,9 +920,11 @@ describe('Execute: defer directive', () => { data: { hero: {}, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { + pending: [{ path: ['hero', 'nestedObject', 'deeperObject'] }], incremental: [ { data: { @@ -951,9 +998,11 @@ describe('Execute: defer directive', () => { }, }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { + pending: [{ path: ['hero', 'nestedObject'] }], incremental: [ { data: { bar: 'bar' }, @@ -964,6 +1013,7 @@ describe('Execute: defer directive', () => { hasNext: true, }, { + pending: [{ path: ['hero', 'nestedObject', 'deeperObject'] }], incremental: [ { data: { baz: 'baz' }, @@ -1020,9 +1070,14 @@ describe('Execute: defer directive', () => { }, }, }, + pending: [ + { path: ['hero'] }, + { path: ['hero', 'nestedObject', 'deeperObject'] }, + ], hasNext: true, }, { + pending: [{ path: ['hero', 'nestedObject', 'deeperObject'] }], incremental: [ { data: { @@ -1093,6 +1148,7 @@ describe('Execute: defer directive', () => { }, }, }, + pending: [{ path: [] }, { path: ['a', 'b'] }], hasNext: true, }, { @@ -1142,6 +1198,7 @@ describe('Execute: defer directive', () => { data: { a: {}, }, + pending: [{ path: [] }, { path: ['a'] }], hasNext: true, }, { @@ -1204,6 +1261,7 @@ describe('Execute: defer directive', () => { data: { a: {}, }, + pending: [{ path: [] }, { path: ['a'] }], hasNext: true, }, { @@ -1266,6 +1324,7 @@ describe('Execute: defer directive', () => { data: { a: {}, }, + pending: [{ path: [] }, { path: ['a'] }], hasNext: true, }, { @@ -1355,6 +1414,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: {}, + pending: [{ path: [] }], hasNext: true, }, { @@ -1403,6 +1463,7 @@ describe('Execute: defer directive', () => { friends: [{ name: 'Han' }, { name: 'Leia' }, { name: 'C-3PO' }], }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1438,6 +1499,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { friends: [{ name: 'Han' }] } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1474,6 +1536,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { friends: [] } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1506,6 +1569,7 @@ describe('Execute: defer directive', () => { friends: [{ name: 'Han' }, { name: 'Leia' }, { name: 'C-3PO' }], }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1553,6 +1617,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { friends: [] } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1586,6 +1651,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { nestedObject: null } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1619,6 +1685,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { nestedObject: { name: 'foo' } } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1651,6 +1718,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { id: '1' } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1693,6 +1761,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { id: '1' } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1771,6 +1840,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { id: '1' } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1823,9 +1893,15 @@ describe('Execute: defer directive', () => { data: { hero: { id: '1' }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { + pending: [ + { path: ['hero', 'friends', 0] }, + { path: ['hero', 'friends', 1] }, + { path: ['hero', 'friends', 2] }, + ], incremental: [ { data: { name: 'slow', friends: [{}, {}, {}] }, @@ -1874,9 +1950,15 @@ describe('Execute: defer directive', () => { data: { hero: { id: '1' }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { + pending: [ + { path: ['hero', 'friends', 0] }, + { path: ['hero', 'friends', 1] }, + { path: ['hero', 'friends', 2] }, + ], incremental: [ { data: { diff --git a/src/execution/__tests__/mutations-test.ts b/src/execution/__tests__/mutations-test.ts index 64262ea0202..13003f7d6be 100644 --- a/src/execution/__tests__/mutations-test.ts +++ b/src/execution/__tests__/mutations-test.ts @@ -237,6 +237,7 @@ describe('Execute: Handles mutation execution ordering', () => { first: {}, second: { theNumber: 2 }, }, + pending: [{ path: ['first'], label: 'defer-label' }], hasNext: true, }, { @@ -312,6 +313,7 @@ describe('Execute: Handles mutation execution ordering', () => { data: { second: { theNumber: 2 }, }, + pending: [{ path: [], label: 'defer-label' }], hasNext: true, }, { diff --git a/src/execution/__tests__/stream-test.ts b/src/execution/__tests__/stream-test.ts index be1e96be5ac..12d4ddd43f2 100644 --- a/src/execution/__tests__/stream-test.ts +++ b/src/execution/__tests__/stream-test.ts @@ -142,6 +142,7 @@ describe('Execute: stream directive', () => { data: { scalarList: ['apple'], }, + pending: [{ path: ['scalarList'] }], hasNext: true, }, { @@ -165,6 +166,7 @@ describe('Execute: stream directive', () => { data: { scalarList: [], }, + pending: [{ path: ['scalarList'] }], hasNext: true, }, { @@ -217,6 +219,7 @@ describe('Execute: stream directive', () => { data: { scalarList: ['apple'], }, + pending: [{ path: ['scalarList'], label: 'scalar-stream' }], hasNext: true, }, { @@ -261,6 +264,7 @@ describe('Execute: stream directive', () => { expectJSON(result).toDeepEqual([ { data: { scalarList: ['apple', 'banana'] }, + pending: [{ path: ['scalarList'] }], hasNext: true, }, { @@ -284,6 +288,7 @@ describe('Execute: stream directive', () => { data: { scalarListList: [['apple', 'apple', 'apple']], }, + pending: [{ path: ['scalarListList'] }], hasNext: true, }, { @@ -333,6 +338,7 @@ describe('Execute: stream directive', () => { }, ], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -369,6 +375,7 @@ describe('Execute: stream directive', () => { data: { friendList: [], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -431,6 +438,7 @@ describe('Execute: stream directive', () => { }, ], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -480,6 +488,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ name: 'Luke', id: '1' }, null], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -517,6 +526,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ name: 'Luke', id: '1' }], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -568,6 +578,7 @@ describe('Execute: stream directive', () => { data: { friendList: [], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -627,6 +638,7 @@ describe('Execute: stream directive', () => { { name: 'Han', id: '2' }, ], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -696,6 +708,7 @@ describe('Execute: stream directive', () => { { name: 'Han', id: '2' }, ], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, }, @@ -769,6 +782,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ name: 'Luke', id: '1' }], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -805,6 +819,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ name: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -851,6 +866,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ name: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -885,6 +901,7 @@ describe('Execute: stream directive', () => { data: { scalarList: ['Luke'], }, + pending: [{ path: ['scalarList'] }], hasNext: true, }, { @@ -928,6 +945,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -978,6 +996,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -1030,6 +1049,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -1069,6 +1089,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -1110,6 +1131,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -1167,6 +1189,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -1234,6 +1257,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -1311,6 +1335,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -1426,6 +1451,10 @@ describe('Execute: stream directive', () => { otherNestedObject: {}, nestedObject: { nestedFriendList: [] }, }, + pending: [ + { path: ['otherNestedObject'] }, + { path: ['nestedObject', 'nestedFriendList'] }, + ], hasNext: true, }, { @@ -1485,6 +1514,7 @@ describe('Execute: stream directive', () => { data: { nestedObject: {}, }, + pending: [{ path: ['nestedObject'] }], hasNext: true, }, { @@ -1537,6 +1567,7 @@ describe('Execute: stream directive', () => { data: { friendList: [], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -1627,6 +1658,7 @@ describe('Execute: stream directive', () => { data: { nestedObject: {}, }, + pending: [{ path: ['nestedObject'] }], hasNext: true, }); @@ -1688,6 +1720,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ id: '1', name: 'Luke' }], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -1747,6 +1780,10 @@ describe('Execute: stream directive', () => { nestedFriendList: [], }, }, + pending: [ + { path: ['nestedObject'] }, + { path: ['nestedObject', 'nestedFriendList'] }, + ], hasNext: true, }, { @@ -1811,6 +1848,7 @@ describe('Execute: stream directive', () => { data: { nestedObject: {}, }, + pending: [{ path: ['nestedObject'] }], hasNext: true, }); @@ -1819,6 +1857,7 @@ describe('Execute: stream directive', () => { const result2 = await result2Promise; expectJSON(result2).toDeepEqual({ value: { + pending: [{ path: ['nestedObject', 'nestedFriendList'] }], incremental: [ { data: { scalarField: 'slow', nestedFriendList: [] }, @@ -1912,6 +1951,10 @@ describe('Execute: stream directive', () => { data: { friendList: [{ id: '1' }], }, + pending: [ + { path: ['friendList', 0], label: 'DeferName' }, + { path: ['friendList'], label: 'stream-label' }, + ], hasNext: true, }); @@ -1920,6 +1963,7 @@ describe('Execute: stream directive', () => { const result2 = await result2Promise; expectJSON(result2).toDeepEqual({ value: { + pending: [{ path: ['friendList', 1], label: 'DeferName' }], incremental: [ { data: { name: 'Luke' }, @@ -2008,6 +2052,10 @@ describe('Execute: stream directive', () => { data: { friendList: [{ id: '1' }], }, + pending: [ + { path: ['friendList', 0], label: 'DeferName' }, + { path: ['friendList'], label: 'stream-label' }, + ], hasNext: true, }); @@ -2016,6 +2064,7 @@ describe('Execute: stream directive', () => { const result2 = await result2Promise; expectJSON(result2).toDeepEqual({ value: { + pending: [{ path: ['friendList', 1], label: 'DeferName' }], incremental: [ { data: { name: 'Luke' }, @@ -2111,6 +2160,7 @@ describe('Execute: stream directive', () => { }, ], }, + pending: [{ path: ['friendList', 0] }, { path: ['friendList'] }], hasNext: true, }); const returnPromise = iterator.return(); @@ -2166,6 +2216,7 @@ describe('Execute: stream directive', () => { }, ], }, + pending: [{ path: ['friendList'] }], hasNext: true, }); @@ -2225,6 +2276,7 @@ describe('Execute: stream directive', () => { }, ], }, + pending: [{ path: ['friendList', 0] }, { path: ['friendList'] }], hasNext: true, });