Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

feat(core): Update supported "Incremental Delivery" format/spec (#3007)

Changed files
+490 -224
.changeset
examples
with-defer-stream-directives
exchanges
packages
+5
.changeset/curly-avocados-buy.md
···
+
---
+
'@urql/core': minor
+
---
+
+
Update support for the "Incremental Delivery" payload specification, accepting the new `incremental` property on execution results, as per the specification. This will expand support for newer APIs implementing the more up-to-date specification.
+5
.changeset/nine-dancers-film.md
···
+
---
+
'@urql/core': minor
+
---
+
+
Update default `Accept` header to include `multipart/mixed` and `application/graphql-response+json`. The former seems to now be a defactor standard-accepted indication for support of the "Incremental Delivery" GraphQL over HTTP spec addition/RFC, and the latter is an updated form of the older `Content-Type` of GraphQL responses, so both the old and new one should now be included.
+15 -12
examples/with-defer-stream-directives/package.json
···
"version": "0.0.0",
"private": true,
"scripts": {
-
"start": "concurrently -k \"vite\" \"node server/index.js\""
+
"server:apollo": "node server/apollo-server.js",
+
"server:yoga": "node server/graphql-yoga.js",
+
"client": "vite",
+
"start": "run-p client server:yoga"
},
"dependencies": {
-
"@urql/core": "^2.3.0",
-
"@urql/exchange-graphcache": "^4.3.0",
-
"graphql": "15.4.0-experimental-stream-defer.1",
+
"@apollo/server": "^4.4.1",
+
"@graphql-yoga/plugin-defer-stream": "^1.7.1",
+
"@urql/core": "^3.1.1",
+
"@urql/exchange-graphcache": "^5.0.9",
+
"graphql": "17.0.0-alpha.2",
+
"graphql-yoga": "^3.7.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
-
"urql": "^2.0.2"
+
"urql": "^3.0.3"
},
"devDependencies": {
-
"@polka/parse": "^1.0.0-next.0",
-
"@vitejs/plugin-react-refresh": "^1.3.3",
-
"concurrently": "^6.2.1",
-
"cors": "^2.8.5",
-
"graphql-helix": "^1.7.0",
-
"polka": "^0.5.2",
-
"vite": "^2.2.4"
+
"@vitejs/plugin-react-refresh": "^1.3.6",
+
"graphql-helix": "^1.13.0",
+
"npm-run-all": "^4.1.5",
+
"vite": "^2.9.15"
}
}
+14
examples/with-defer-stream-directives/server/apollo-server.js
···
+
// NOTE: This currently fails because responses for @defer/@stream are not sent
+
// as multipart responses, but the request fails silently with an empty JSON response payload
+
+
const { ApolloServer } = require('@apollo/server');
+
const { startStandaloneServer } = require('@apollo/server/standalone');
+
const { schema } = require('./schema');
+
+
const server = new ApolloServer({ schema });
+
+
startStandaloneServer(server, {
+
listen: {
+
port: 3004,
+
},
+
});
+13
examples/with-defer-stream-directives/server/graphql-yoga.js
···
+
const { createYoga } = require('graphql-yoga');
+
const { useDeferStream } = require('@graphql-yoga/plugin-defer-stream');
+
const { createServer } = require('http');
+
const { schema } = require('./schema');
+
+
const yoga = createYoga({
+
schema,
+
plugins: [useDeferStream()],
+
});
+
+
const server = createServer(yoga);
+
+
server.listen(3004);
-126
examples/with-defer-stream-directives/server/index.js
···
-
// Credits to https://github.com/maraisr/meros/blob/main/examples/relay-with-helix/server.js
-
-
/* eslint-disable @typescript-eslint/no-var-requires, es5/no-generators, no-console */
-
const polka = require('polka');
-
const { json } = require('@polka/parse');
-
const cors = require('cors')();
-
const { getGraphQLParameters, processRequest } = require('graphql-helix');
-
const {
-
GraphQLList,
-
GraphQLObjectType,
-
GraphQLSchema,
-
GraphQLString,
-
} = require('graphql');
-
-
const schema = new GraphQLSchema({
-
query: new GraphQLObjectType({
-
name: 'Query',
-
fields: () => ({
-
alphabet: {
-
type: new GraphQLList(
-
new GraphQLObjectType({
-
name: 'Alphabet',
-
fields: {
-
char: {
-
type: GraphQLString,
-
},
-
},
-
})
-
),
-
resolve: async function* () {
-
for (let letter = 65; letter <= 90; letter++) {
-
await new Promise(resolve => setTimeout(resolve, 500));
-
yield { char: String.fromCharCode(letter) };
-
}
-
},
-
},
-
song: {
-
type: new GraphQLObjectType({
-
name: 'Song',
-
fields: () => ({
-
firstVerse: {
-
type: GraphQLString,
-
resolve: () => "Now I know my ABC's.",
-
},
-
secondVerse: {
-
type: GraphQLString,
-
resolve: () =>
-
new Promise(resolve =>
-
setTimeout(
-
() => resolve("Next time won't you sing with me?"),
-
5000
-
)
-
),
-
},
-
}),
-
}),
-
resolve: () =>
-
new Promise(resolve => setTimeout(() => resolve('goodbye'), 1000)),
-
},
-
}),
-
}),
-
});
-
-
polka()
-
.use(cors, json())
-
.use('/graphql', async (req, res) => {
-
const request = {
-
body: req.body,
-
headers: req.headers,
-
method: req.method,
-
query: req.query,
-
};
-
-
let { operationName, query, variables } = getGraphQLParameters(request);
-
-
const result = await processRequest({
-
operationName,
-
query,
-
variables,
-
request,
-
schema,
-
});
-
-
if (result.type === 'RESPONSE') {
-
result.headers.forEach(({ name, value }) => res.setHeader(name, value));
-
res.writeHead(result.status, {
-
'Content-Type': 'application/json',
-
});
-
res.end(JSON.stringify(result.payload));
-
} else if (result.type === 'MULTIPART_RESPONSE') {
-
res.writeHead(200, {
-
Connection: 'keep-alive',
-
'Content-Type': 'multipart/mixed; boundary="-"',
-
'Transfer-Encoding': 'chunked',
-
});
-
-
req.on('close', () => {
-
result.unsubscribe();
-
});
-
-
res.write('---');
-
-
await result.subscribe(result => {
-
const chunk = Buffer.from(JSON.stringify(result), 'utf8');
-
const data = [
-
'',
-
'Content-Type: application/json; charset=utf-8',
-
'',
-
chunk,
-
];
-
-
if (result.hasNext) {
-
data.push('---');
-
}
-
-
res.write(data.join('\r\n'));
-
});
-
-
res.write('\r\n-----\r\n');
-
res.end();
-
}
-
})
-
.listen(3004, err => {
-
if (err) throw err;
-
console.log(`> Running on localhost:3004`);
-
});
+57
examples/with-defer-stream-directives/server/schema.js
···
+
const {
+
GraphQLList,
+
GraphQLObjectType,
+
GraphQLSchema,
+
GraphQLString,
+
} = require('graphql');
+
+
const schema = new GraphQLSchema({
+
query: new GraphQLObjectType({
+
name: 'Query',
+
fields: () => ({
+
alphabet: {
+
type: new GraphQLList(
+
new GraphQLObjectType({
+
name: 'Alphabet',
+
fields: {
+
char: {
+
type: GraphQLString,
+
},
+
},
+
})
+
),
+
resolve: async function* () {
+
for (let letter = 65; letter <= 90; letter++) {
+
await new Promise(resolve => setTimeout(resolve, 500));
+
yield { char: String.fromCharCode(letter) };
+
}
+
},
+
},
+
song: {
+
type: new GraphQLObjectType({
+
name: 'Song',
+
fields: () => ({
+
firstVerse: {
+
type: GraphQLString,
+
resolve: () => "Now I know my ABC's.",
+
},
+
secondVerse: {
+
type: GraphQLString,
+
resolve: () =>
+
new Promise(resolve =>
+
setTimeout(
+
() => resolve("Next time won't you sing with me?"),
+
5000
+
)
+
),
+
},
+
}),
+
}),
+
resolve: () =>
+
new Promise(resolve => setTimeout(() => resolve('goodbye'), 1000)),
+
},
+
}),
+
}),
+
});
+
+
module.exports = { schema };
+1 -1
examples/with-defer-stream-directives/src/Songs.jsx
···
firstVerse
...secondVerseFields @defer
}
-
alphabet @stream(initial_count: 3) {
+
alphabet @stream(initialCount: 3) {
char
}
}
+3 -15
exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
···
-
// Vitest Snapshot v1
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`on error > returns error data 1`] = `
{
···
}
`;
-
exports[`on success > uses a file when given 2`] = `
-
{
-
"accept": "application/graphql+json, application/json",
-
}
-
`;
-
-
exports[`on success > uses a file when given 3`] = `FormData {}`;
+
exports[`on success > uses a file when given 2`] = `FormData {}`;
exports[`on success > uses multiple files when given 1`] = `
{
···
}
`;
-
exports[`on success > uses multiple files when given 2`] = `
-
{
-
"accept": "application/graphql+json, application/json",
-
}
-
`;
-
-
exports[`on success > uses multiple files when given 3`] = `FormData {}`;
+
exports[`on success > uses multiple files when given 2`] = `FormData {}`;
-2
exchanges/multipart-fetch/src/multipartFetchExchange.test.ts
···
expect(data).toMatchSnapshot();
expect(fetchOptions).toHaveBeenCalled();
-
expect(fetch.mock.calls[0][1].headers).toMatchSnapshot();
expect(fetch.mock.calls[0][1].body).toMatchSnapshot();
});
···
expect(data).toMatchSnapshot();
expect(fetchOptions).toHaveBeenCalled();
-
expect(fetch.mock.calls[0][1].headers).toMatchSnapshot();
expect(fetch.mock.calls[0][1].body).toMatchSnapshot();
});
+1 -1
packages/core/src/__snapshots__/client.test.ts.snap
···
-
// Vitest Snapshot v1
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`createClient / Client > passes snapshot 1`] = `
Client2 {
+1 -1
packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
···
-
// Vitest Snapshot v1
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`on error > returns error data 1`] = `
{
+1 -1
packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap
···
-
// Vitest Snapshot v1
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`should return response data from forwardSubscription observable 1`] = `
{
+1 -1
packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
···
-
// Vitest Snapshot v1
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`on error > ignores the error when a result is available 1`] = `
{
+2 -1
packages/core/src/internal/fetchOptions.ts
···
const useGETMethod =
operation.kind === 'query' && !!operation.context.preferGetMethod;
const headers: HeadersInit = {
-
accept: 'application/graphql+json, application/json',
+
accept:
+
'multipart/mixed, application/graphql-response+json, application/graphql+json, application/json',
};
if (!useGETMethod) headers['content-type'] = 'application/json';
const extraOptions =
+18 -6
packages/core/src/internal/fetchSource.test.ts
···
done: false,
value: Buffer.from(
wrap({
-
path: ['author', 'todos', 1],
-
data: { id: '2', text: 'defer', __typename: 'Todo' },
+
incremental: [
+
{
+
path: ['author', 'todos', 1],
+
data: { id: '2', text: 'defer', __typename: 'Todo' },
+
},
+
],
hasNext: true,
})
),
···
done: false,
value: Buffer.from(
wrap({
-
path: ['author'],
-
data: { name: 'Steve' },
+
incremental: [
+
{
+
path: ['author'],
+
data: { name: 'Steve' },
+
},
+
],
hasNext: true,
})
),
···
done: false,
value: Buffer.from(
wrap({
-
path: ['author', 'address'],
-
data: { street: 'home' },
+
incremental: [
+
{
+
path: ['author', 'address'],
+
data: { street: 'home' },
+
},
+
],
hasNext: true,
})
),
+19 -17
packages/core/src/types.ts
···
__apiType?: (variables: Variables) => Result;
}
-
export type ExecutionResult =
-
| {
-
errors?:
-
| Array<Partial<GraphQLError> | string | Error>
-
| readonly GraphQLError[];
-
data?: null | Record<string, any>;
-
extensions?: Record<string, any>;
-
hasNext?: boolean;
-
}
-
| {
-
errors?:
-
| Array<Partial<GraphQLError> | string | Error>
-
| readonly GraphQLError[];
-
data: any;
-
path: (string | number)[];
-
hasNext?: boolean;
-
};
+
type ErrorLike = Partial<GraphQLError> | Error;
+
type Extensions = Record<string, any>;
+
+
export interface IncrementalPayload {
+
label?: string | null;
+
path: readonly (string | number)[];
+
data?: Record<string, unknown> | null;
+
items?: readonly unknown[] | null;
+
errors?: ErrorLike[] | readonly ErrorLike[];
+
extensions?: Extensions;
+
}
+
+
export interface ExecutionResult {
+
incremental?: IncrementalPayload[];
+
data?: null | Record<string, any>;
+
errors?: ErrorLike[] | readonly ErrorLike[];
+
extensions?: Extensions;
+
hasNext?: boolean;
+
}
export type PromisifiedSource<T = any> = Source<T> & {
toPromise: () => Promise<T>;
+1 -1
packages/core/src/utils/__snapshots__/error.test.ts.snap
···
-
// Vitest Snapshot v1
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`CombinedError > behaves like a normal Error 1`] = `"[Network] test"`;
-7
packages/core/src/utils/error.test.ts
···
expect(err.graphQLErrors).toEqual(graphQLErrors);
});
-
it('passes graphQLErrors through as a last resort', () => {
-
const graphQLErrors = [{ x: 'y' }] as any;
-
const err = new CombinedError({ graphQLErrors });
-
-
expect(err.graphQLErrors).toEqual(graphQLErrors);
-
});
-
it('accepts a response that is attached to the resulting error', () => {
const response = {};
const err = new CombinedError({
+3 -3
packages/core/src/utils/error.ts
···
};
const rehydrateGraphQlError = (error: any): GraphQLError => {
-
if (typeof error === 'string') {
-
return new GraphQLError(error);
+
if (error instanceof GraphQLError) {
+
return error;
} else if (typeof error === 'object' && error.message) {
return new GraphQLError(
error.message,
···
error.extensions || {}
);
} else {
-
return error as any;
+
return new GraphQLError(error as any);
}
};
+265 -1
packages/core/src/utils/result.test.ts
···
import { describe, it, expect } from 'vitest';
+
import { OperationResult } from '../types';
import { queryOperation } from '../test-utils';
-
import { makeResult } from './result';
+
import { makeResult, mergeResultPatch } from './result';
describe('makeResult', () => {
it('adds extensions and errors correctly', () => {
···
);
});
});
+
+
describe('mergeResultPatch', () => {
+
it('should ignore invalid patches', () => {
+
const prevResult: OperationResult = {
+
operation: queryOperation,
+
data: {
+
__typename: 'Query',
+
items: [
+
{
+
__typename: 'Item',
+
id: 'id',
+
},
+
],
+
},
+
};
+
+
const merged = mergeResultPatch(prevResult, {
+
incremental: [
+
{
+
data: undefined,
+
path: ['a'],
+
},
+
{
+
items: null,
+
path: ['b'],
+
},
+
],
+
});
+
+
expect(merged.data).toStrictEqual({
+
__typename: 'Query',
+
items: [
+
{
+
__typename: 'Item',
+
id: 'id',
+
},
+
],
+
});
+
});
+
+
it('should apply incremental defer patches', () => {
+
const prevResult: OperationResult = {
+
operation: queryOperation,
+
data: {
+
__typename: 'Query',
+
items: [
+
{
+
__typename: 'Item',
+
id: 'id',
+
child: undefined,
+
},
+
],
+
},
+
};
+
+
const patch = { __typename: 'Child' };
+
+
const merged = mergeResultPatch(prevResult, {
+
incremental: [
+
{
+
data: patch,
+
path: ['items', 0, 'child'],
+
},
+
],
+
});
+
+
expect(merged.data.items[0]).not.toBe(prevResult.data.items[0]);
+
expect(merged.data.items[0].child).toBe(patch);
+
expect(merged.data).toStrictEqual({
+
__typename: 'Query',
+
items: [
+
{
+
__typename: 'Item',
+
id: 'id',
+
child: patch,
+
},
+
],
+
});
+
});
+
+
it('should handle null incremental defer patches', () => {
+
const prevResult: OperationResult = {
+
operation: queryOperation,
+
data: {
+
__typename: 'Query',
+
item: undefined,
+
},
+
};
+
+
const merged = mergeResultPatch(prevResult, {
+
incremental: [
+
{
+
data: null,
+
path: ['item'],
+
},
+
],
+
});
+
+
expect(merged.data).not.toBe(prevResult.data);
+
expect(merged.data.item).toBe(null);
+
});
+
+
it('should apply incremental stream patches', () => {
+
const prevResult: OperationResult = {
+
operation: queryOperation,
+
data: {
+
__typename: 'Query',
+
items: [{ __typename: 'Item' }],
+
},
+
};
+
+
const patch = { __typename: 'Item' };
+
+
const merged = mergeResultPatch(prevResult, {
+
incremental: [
+
{
+
items: [patch],
+
path: ['items', 1],
+
},
+
],
+
});
+
+
expect(merged.data.items).not.toBe(prevResult.data.items);
+
expect(merged.data.items[0]).toBe(prevResult.data.items[0]);
+
expect(merged.data.items[1]).toBe(patch);
+
expect(merged.data).toStrictEqual({
+
__typename: 'Query',
+
items: [{ __typename: 'Item' }, { __typename: 'Item' }],
+
});
+
});
+
+
it('should handle null incremental stream patches', () => {
+
const prevResult: OperationResult = {
+
operation: queryOperation,
+
data: {
+
__typename: 'Query',
+
items: [{ __typename: 'Item' }],
+
},
+
};
+
+
const merged = mergeResultPatch(prevResult, {
+
incremental: [
+
{
+
items: null,
+
path: ['items', 1],
+
},
+
],
+
});
+
+
expect(merged.data.items).not.toBe(prevResult.data.items);
+
expect(merged.data.items[0]).toBe(prevResult.data.items[0]);
+
expect(merged.data).toStrictEqual({
+
__typename: 'Query',
+
items: [{ __typename: 'Item' }],
+
});
+
});
+
+
it('should merge extensions from each patch', () => {
+
const prevResult: OperationResult = {
+
operation: queryOperation,
+
data: {
+
__typename: 'Query',
+
},
+
extensions: {
+
base: true,
+
},
+
};
+
+
const merged = mergeResultPatch(prevResult, {
+
incremental: [
+
{
+
data: null,
+
path: ['item'],
+
extensions: {
+
patch: true,
+
},
+
},
+
],
+
});
+
+
expect(merged.extensions).toStrictEqual({
+
base: true,
+
patch: true,
+
});
+
});
+
+
it('should combine errors from each patch', () => {
+
const prevResult: OperationResult = makeResult(queryOperation, {
+
errors: ['base'],
+
});
+
+
const merged = mergeResultPatch(prevResult, {
+
incremental: [
+
{
+
data: null,
+
path: ['item'],
+
errors: ['patch'],
+
},
+
],
+
});
+
+
expect(merged.error).toMatchInlineSnapshot(`
+
[CombinedError: [GraphQL] base
+
[GraphQL] patch]
+
`);
+
});
+
+
it('should preserve all data for noop patches', () => {
+
const prevResult: OperationResult = {
+
operation: queryOperation,
+
data: {
+
__typename: 'Query',
+
},
+
extensions: {
+
base: true,
+
},
+
};
+
+
const merged = mergeResultPatch(prevResult, {
+
hasNext: false,
+
});
+
+
expect(merged.data).toStrictEqual({
+
__typename: 'Query',
+
});
+
});
+
+
it('handles the old version of the incremental payload spec (DEPRECATED)', () => {
+
const prevResult: OperationResult = {
+
operation: queryOperation,
+
data: {
+
__typename: 'Query',
+
items: [
+
{
+
__typename: 'Item',
+
id: 'id',
+
child: undefined,
+
},
+
],
+
},
+
};
+
+
const patch = { __typename: 'Child' };
+
+
const merged = mergeResultPatch(prevResult, {
+
data: patch,
+
path: ['items', 0, 'child'],
+
} as any);
+
+
expect(merged.data.items[0]).not.toBe(prevResult.data.items[0]);
+
expect(merged.data.items[0].child).toBe(patch);
+
expect(merged.data).toStrictEqual({
+
__typename: 'Query',
+
items: [
+
{
+
__typename: 'Item',
+
id: 'id',
+
child: patch,
+
},
+
],
+
});
+
});
+
});
+65 -28
packages/core/src/utils/result.ts
···
-
import { ExecutionResult, Operation, OperationResult } from '../types';
+
import {
+
ExecutionResult,
+
Operation,
+
OperationResult,
+
IncrementalPayload,
+
} from '../types';
import { CombinedError } from './error';
export const makeResult = (
···
result: ExecutionResult,
response?: any
): OperationResult => {
-
if ((!('data' in result) && !('errors' in result)) || 'path' in result) {
+
if (
+
(!('data' in result) && !('errors' in result)) ||
+
'incremental' in result
+
) {
throw new Error('No Content');
}
···
export const mergeResultPatch = (
prevResult: OperationResult,
-
patch: ExecutionResult,
+
nextResult: ExecutionResult,
response?: any
): OperationResult => {
-
const result = { ...prevResult };
-
result.hasNext = !!patch.hasNext;
+
let data: ExecutionResult['data'];
+
let hasExtensions = !!prevResult.extensions || !!nextResult.extensions;
+
const extensions = { ...prevResult.extensions, ...nextResult.extensions };
+
const errors = prevResult.error ? prevResult.error.graphQLErrors : [];
-
if (!('path' in patch)) {
-
if ('data' in patch) result.data = patch.data;
-
return result;
+
let incremental = nextResult.incremental;
+
+
// NOTE: We handle the old version of the incremental delivery payloads as well
+
if ('path' in nextResult) {
+
incremental = [
+
{
+
data: nextResult.data,
+
path: nextResult.path,
+
} as IncrementalPayload,
+
];
}
-
if (Array.isArray(patch.errors)) {
-
result.error = new CombinedError({
-
graphQLErrors: result.error
-
? [...result.error.graphQLErrors, ...patch.errors]
-
: patch.errors,
-
response,
-
});
-
}
+
if (incremental) {
+
data = { ...prevResult.data };
+
for (const patch of incremental) {
+
if (Array.isArray(patch.errors)) {
+
errors.push(...(patch.errors as any));
+
}
+
+
if (patch.extensions) {
+
Object.assign(extensions, patch.extensions);
+
hasExtensions = true;
+
}
-
let part: Record<string, any> | Array<any> = (result.data = {
-
...result.data,
-
});
+
let prop: string | number = patch.path[0];
+
let part: Record<string, any> | Array<any> = data as object;
+
for (let i = 1, l = patch.path.length; i < l; prop = patch.path[i++]) {
+
part = part[prop] = Array.isArray(part[prop])
+
? [...part[prop]]
+
: { ...part[prop] };
+
}
-
let i = 0;
-
let prop: string | number;
-
while (i < patch.path.length) {
-
prop = patch.path[i++];
-
part = part[prop] = Array.isArray(part[prop])
-
? [...part[prop]]
-
: { ...part[prop] };
+
if (Array.isArray(patch.items)) {
+
const startIndex = +prop >= 0 ? (prop as number) : 0;
+
for (let i = 0, l = patch.items.length; i < l; i++)
+
part[startIndex + i] = patch.items[i];
+
} else if (patch.data !== undefined) {
+
part[prop] =
+
part[prop] && patch.data
+
? { ...part[prop], ...patch.data }
+
: patch.data;
+
}
+
}
+
} else {
+
data = nextResult.data || prevResult.data;
}
-
Object.assign(part, patch.data);
-
return result;
+
return {
+
operation: prevResult.operation,
+
data,
+
error: errors.length
+
? new CombinedError({ graphQLErrors: errors, response })
+
: undefined,
+
extensions: hasExtensions ? extensions : undefined,
+
hasNext: !!nextResult.hasNext,
+
};
};
export const makeErrorResult = (