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

refactor(core): Refactor hasNext/stale on OperationResult to be required (#3061)

Changed files
+266 -56
.changeset
exchanges
packages
core
react-urql
src
test-utils
svelte-urql
src
+7
.changeset/four-boxes-impress.md
···
···
+
---
+
'@urql/exchange-graphcache': major
+
'@urql/svelte': major
+
'@urql/core': major
+
---
+
+
Update `OperationResult.hasNext` and `OperationResult.stale` to be required fields. If you have a custom exchange creating results, you'll have to add these fields or use the `makeResult`, `mergeResultPatch`, or `makeErrorResult` helpers.
+7 -2
exchanges/auth/src/authExchange.test.ts
···
import { vi, expect, it } from 'vitest';
import { print } from 'graphql';
-
import { queryOperation } from '../../../packages/core/src/test-utils';
import { authExchange } from './authExchange';
const makeExchangeArgs = () => {
const operations: Operation[] = [];
const result = vi.fn(
-
(operation: Operation): OperationResult => ({ operation })
);
return {
···
await new Promise(resolve => setTimeout(resolve));
result.mockReturnValueOnce({
operation: queryOperation,
error: new CombinedError({
graphQLErrors: [{ message: 'Oops' }],
}),
···
import { vi, expect, it } from 'vitest';
import { print } from 'graphql';
+
import {
+
queryResponse,
+
queryOperation,
+
} from '../../../packages/core/src/test-utils';
import { authExchange } from './authExchange';
const makeExchangeArgs = () => {
const operations: Operation[] = [];
const result = vi.fn(
+
(operation: Operation): OperationResult => ({ ...queryResponse, operation })
);
return {
···
await new Promise(resolve => setTimeout(resolve));
result.mockReturnValueOnce({
+
...queryResponse,
operation: queryOperation,
+
data: undefined,
error: new CombinedError({
graphQLErrors: [{ message: 'Oops' }],
}),
+3
exchanges/context/src/context.test.ts
···
ExchangeIO,
} from '@urql/core';
import { contextExchange } from './context';
const queryOne = gql`
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
return {
operation: forwardOp,
data: queryOneData,
};
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
return {
operation: forwardOp,
data: queryOneData,
};
···
ExchangeIO,
} from '@urql/core';
+
import { queryResponse } from '../../../packages/core/src/test-utils';
import { contextExchange } from './context';
const queryOne = gql`
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
return {
+
...queryResponse,
operation: forwardOp,
data: queryOneData,
};
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
return {
+
...queryResponse,
operation: forwardOp,
data: queryOneData,
};
+1
exchanges/execute/src/execute.test.ts
···
operation: queryOperation,
data: mockHttpResponseData,
hasNext: false,
});
});
});
···
operation: queryOperation,
data: mockHttpResponseData,
hasNext: false,
+
stale: false,
});
});
});
+107 -27
exchanges/graphcache/src/cacheExchange.test.ts
···
OperationResult,
CombinedError,
} from '@urql/core';
import { vi, expect, it, describe } from 'vitest';
import {
···
} from 'wonka';
import { minifyIntrospectionQuery } from '@urql/introspection';
import { cacheExchange } from './cacheExchange';
const queryOne = gql`
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
expect(forwardOp.key).toBe(op.key);
-
return { operation: forwardOp, data: expected };
}
);
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
expect(forwardOp.key).toBe(op.key);
-
return { operation: forwardOp, data: queryOneData };
}
);
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: opOne, data: queryOneData };
} else if (forwardOp.key === 2) {
-
return { operation: opMultiple, data: queryMultipleData };
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: opOne, data: queryByIdDataA };
} else if (forwardOp.key === 2) {
-
return { operation: opTwo, data: queryByIdDataB };
} else if (forwardOp.key === 3) {
-
return { operation: opMutation, data: mutationData };
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: opOne, data: queryOneData };
} else if (forwardOp.key === 2) {
-
return { operation: opUnrelated, data: queryUnrelatedData };
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: opMutation, data: mutationData };
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: opMutation, data: mutationData };
}
return undefined as any;
···
});
const queryResult: OperationResult = {
operation,
data: {
__typename: 'Query',
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: opOne, data: queryOneData };
} else if (forwardOp.key === 2) {
-
return { operation: opMutation, data: mutationData };
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: opOne, data: queryOneData };
} else if (forwardOp.key === 2) {
-
return { operation: opMutationOne, data: mutationData };
} else if (forwardOp.key === 3) {
-
return { operation: opMutationTwo, data: mutationData };
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: opOne, data: queryOneData };
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: opOne, data: authorsQueryData };
} else if (forwardOp.key === 2) {
return {
operation: opMutation,
error: 'error' as any,
data: { __typename: 'Mutation', addAuthor: null },
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: opOne, data: queryOneData };
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: opOne, data: queryOneData };
} else if (forwardOp.key === 2) {
-
return { operation: opMutation, data: mutationData };
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: queryOperation, data: queryData };
} else if (forwardOp.key === 2) {
-
return { operation: mutationOperation, data: mutationData };
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: initialQueryOperation, data: queryData };
} else if (forwardOp.key === 2) {
-
return { operation: queryOperation, data: queryData };
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
-
return { operation: initialQueryOperation, data: queryData };
} else if (forwardOp.key === 2) {
-
return { operation: queryOperation, data: queryData };
}
return undefined as any;
···
const result = (operation: Operation): Source<OperationResult> =>
pipe(
fromValue({
operation,
data: {
__typename: 'Query',
···
nextOp(queryOpA);
nextRes({
operation: queryOpA,
data: {
__typename: 'Query',
···
nextOp(queryOpB);
nextRes({
operation: queryOpA,
data: {
__typename: 'Query',
···
expect(data).toHaveProperty('node.name', 'query a');
nextRes({
operation: mutationOp,
data: {
__typename: 'Mutation',
···
expect(data).toHaveProperty('node.name', 'mutation');
nextRes({
operation: queryOpB,
data: {
__typename: 'Query',
···
nextOp(mutationOp);
nextRes({
operation: queryOp,
data: {
__typename: 'Query',
···
expect(data).toHaveProperty('node.name', 'optimistic');
nextRes({
operation: mutationOp,
data: {
__typename: 'Query',
···
nextOp(subscriptionOp);
nextRes({
operation: queryOpA,
data: {
__typename: 'Query',
···
});
nextRes({
operation: subscriptionOp,
data: {
node: {
···
nextOp(subscriptionOp);
nextRes({
operation: queryOpA,
data: {
__typename: 'Query',
···
nextOp(mutationOp);
nextRes({
operation: mutationOp,
data: {
node: {
···
});
nextRes({
operation: subscriptionOp,
data: {
node: {
···
});
nextRes({
operation: subscriptionOp,
data: {
node: {
···
nextOp(normalOp);
nextRes({
operation: deferredOp,
data: {
__typename: 'Query',
···
expect(deferredData).not.toHaveProperty('deferred');
nextRes({
operation: normalOp,
data: {
__typename: 'Query',
···
expect(combinedData).toHaveProperty('node.id', 2);
nextRes({
operation: deferredOp,
data: {
__typename: 'Query',
···
OperationResult,
CombinedError,
} from '@urql/core';
+
import { vi, expect, it, describe } from 'vitest';
import {
···
} from 'wonka';
import { minifyIntrospectionQuery } from '@urql/introspection';
+
import { queryResponse } from '../../../packages/core/src/test-utils';
import { cacheExchange } from './cacheExchange';
const queryOne = gql`
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
expect(forwardOp.key).toBe(op.key);
+
return { ...queryResponse, operation: forwardOp, data: expected };
}
);
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
expect(forwardOp.key).toBe(op.key);
+
return { ...queryResponse, operation: forwardOp, data: queryOneData };
}
);
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return { ...queryResponse, operation: opOne, data: queryOneData };
} else if (forwardOp.key === 2) {
+
return {
+
...queryResponse,
+
operation: opMultiple,
+
data: queryMultipleData,
+
};
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return { ...queryResponse, operation: opOne, data: queryByIdDataA };
} else if (forwardOp.key === 2) {
+
return { ...queryResponse, operation: opTwo, data: queryByIdDataB };
} else if (forwardOp.key === 3) {
+
return {
+
...queryResponse,
+
operation: opMutation,
+
data: mutationData,
+
};
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return { ...queryResponse, operation: opOne, data: queryOneData };
} else if (forwardOp.key === 2) {
+
return {
+
...queryResponse,
+
operation: opUnrelated,
+
data: queryUnrelatedData,
+
};
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return {
+
...queryResponse,
+
operation: opMutation,
+
data: mutationData,
+
};
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return {
+
...queryResponse,
+
operation: opMutation,
+
data: mutationData,
+
};
}
return undefined as any;
···
});
const queryResult: OperationResult = {
+
...queryResponse,
operation,
data: {
__typename: 'Query',
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return { ...queryResponse, operation: opOne, data: queryOneData };
} else if (forwardOp.key === 2) {
+
return {
+
...queryResponse,
+
operation: opMutation,
+
data: mutationData,
+
};
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return { ...queryResponse, operation: opOne, data: queryOneData };
} else if (forwardOp.key === 2) {
+
return {
+
...queryResponse,
+
operation: opMutationOne,
+
data: mutationData,
+
};
} else if (forwardOp.key === 3) {
+
return {
+
...queryResponse,
+
operation: opMutationTwo,
+
data: mutationData,
+
};
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return { ...queryResponse, operation: opOne, data: queryOneData };
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return { ...queryResponse, operation: opOne, data: authorsQueryData };
} else if (forwardOp.key === 2) {
return {
+
...queryResponse,
operation: opMutation,
error: 'error' as any,
data: { __typename: 'Mutation', addAuthor: null },
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return { ...queryResponse, operation: opOne, data: queryOneData };
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return { ...queryResponse, operation: opOne, data: queryOneData };
} else if (forwardOp.key === 2) {
+
return {
+
...queryResponse,
+
operation: opMutation,
+
data: mutationData,
+
};
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return {
+
...queryResponse,
+
operation: queryOperation,
+
data: queryData,
+
};
} else if (forwardOp.key === 2) {
+
return {
+
...queryResponse,
+
operation: mutationOperation,
+
data: mutationData,
+
};
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return {
+
...queryResponse,
+
operation: initialQueryOperation,
+
data: queryData,
+
};
} else if (forwardOp.key === 2) {
+
return {
+
...queryResponse,
+
operation: queryOperation,
+
data: queryData,
+
};
}
return undefined as any;
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
+
return {
+
...queryResponse,
+
operation: initialQueryOperation,
+
data: queryData,
+
};
} else if (forwardOp.key === 2) {
+
return {
+
...queryResponse,
+
operation: queryOperation,
+
data: queryData,
+
};
}
return undefined as any;
···
const result = (operation: Operation): Source<OperationResult> =>
pipe(
fromValue({
+
...queryResponse,
operation,
data: {
__typename: 'Query',
···
nextOp(queryOpA);
nextRes({
+
...queryResponse,
operation: queryOpA,
data: {
__typename: 'Query',
···
nextOp(queryOpB);
nextRes({
+
...queryResponse,
operation: queryOpA,
data: {
__typename: 'Query',
···
expect(data).toHaveProperty('node.name', 'query a');
nextRes({
+
...queryResponse,
operation: mutationOp,
data: {
__typename: 'Mutation',
···
expect(data).toHaveProperty('node.name', 'mutation');
nextRes({
+
...queryResponse,
operation: queryOpB,
data: {
__typename: 'Query',
···
nextOp(mutationOp);
nextRes({
+
...queryResponse,
operation: queryOp,
data: {
__typename: 'Query',
···
expect(data).toHaveProperty('node.name', 'optimistic');
nextRes({
+
...queryResponse,
operation: mutationOp,
data: {
__typename: 'Query',
···
nextOp(subscriptionOp);
nextRes({
+
...queryResponse,
operation: queryOpA,
data: {
__typename: 'Query',
···
});
nextRes({
+
...queryResponse,
operation: subscriptionOp,
data: {
node: {
···
nextOp(subscriptionOp);
nextRes({
+
...queryResponse,
operation: queryOpA,
data: {
__typename: 'Query',
···
nextOp(mutationOp);
nextRes({
+
...queryResponse,
operation: mutationOp,
data: {
node: {
···
});
nextRes({
+
...queryResponse,
operation: subscriptionOp,
data: {
node: {
···
});
nextRes({
+
...queryResponse,
operation: subscriptionOp,
data: {
node: {
···
nextOp(normalOp);
nextRes({
+
...queryResponse,
operation: deferredOp,
data: {
__typename: 'Query',
···
expect(deferredData).not.toHaveProperty('deferred');
nextRes({
+
...queryResponse,
operation: normalOp,
data: {
__typename: 'Query',
···
expect(combinedData).toHaveProperty('node.id', 2);
nextRes({
+
...queryResponse,
operation: deferredOp,
data: {
__typename: 'Query',
+3 -2
exchanges/graphcache/src/cacheExchange.ts
···
import { Store, noopDataState, hydrateData, reserveLayer } from './store';
import { Data, Dependencies, CacheExchangeOpts } from './types';
-
type OperationResultWithMeta = OperationResult & {
outcome: CacheOutcome;
dependencies: Dependencies;
-
};
type Operations = Set<number>;
type OperationMap = Map<number, Operation>;
···
import { Store, noopDataState, hydrateData, reserveLayer } from './store';
import { Data, Dependencies, CacheExchangeOpts } from './types';
+
interface OperationResultWithMeta extends Partial<OperationResult> {
+
operation: Operation;
outcome: CacheOutcome;
dependencies: Dependencies;
+
}
type Operations = Set<number>;
type OperationMap = Map<number, Operation>;
+8 -2
exchanges/graphcache/src/offlineExchange.test.ts
···
import { vi, expect, it, describe } from 'vitest';
import { pipe, map, makeSubject, tap, publish } from 'wonka';
import { offlineExchange } from './offlineExchange';
const mutationOne = gql`
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
expect(forwardOp.key).toBe(op.key);
-
return { operation: forwardOp, data: mutationOneData };
}
);
···
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === queryOp.key) {
onlineSpy.mockReturnValueOnce(true);
-
return { operation: forwardOp, data: queryOneData };
} else {
onlineSpy.mockReturnValueOnce(false);
return {
operation: forwardOp,
// @ts-ignore
error: { networkError: new Error('failed to fetch') },
···
import { vi, expect, it, describe } from 'vitest';
import { pipe, map, makeSubject, tap, publish } from 'wonka';
+
import { queryResponse } from '../../../packages/core/src/test-utils';
import { offlineExchange } from './offlineExchange';
const mutationOne = gql`
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
expect(forwardOp.key).toBe(op.key);
+
return {
+
...queryResponse,
+
operation: forwardOp,
+
data: mutationOneData,
+
};
}
);
···
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === queryOp.key) {
onlineSpy.mockReturnValueOnce(true);
+
return { ...queryResponse, operation: forwardOp, data: queryOneData };
} else {
onlineSpy.mockReturnValueOnce(false);
return {
+
...queryResponse,
operation: forwardOp,
// @ts-ignore
error: { networkError: new Error('failed to fetch') },
+7
exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
···
"data": undefined,
"error": [CombinedError: [Network] No Content],
"extensions": undefined,
"operation": {
"context": {
"fetchOptions": {
···
"name": "Clara",
},
},
}
`;
···
"data": undefined,
"error": [CombinedError: [Network] No Content],
"extensions": undefined,
"operation": {
"context": {
"fetchOptions": [MockFunction spy] {
···
"name": "Clara",
},
},
}
`;
···
"name": "Clara",
},
},
}
`;
···
"picture": File {},
},
},
}
`;
···
],
},
},
}
`;
···
"data": undefined,
"error": [CombinedError: [Network] No Content],
"extensions": undefined,
+
"hasNext": false,
"operation": {
"context": {
"fetchOptions": {
···
"name": "Clara",
},
},
+
"stale": false,
}
`;
···
"data": undefined,
"error": [CombinedError: [Network] No Content],
"extensions": undefined,
+
"hasNext": false,
"operation": {
"context": {
"fetchOptions": [MockFunction spy] {
···
"name": "Clara",
},
},
+
"stale": false,
}
`;
···
"name": "Clara",
},
},
+
"stale": false,
}
`;
···
"picture": File {},
},
},
+
"stale": false,
}
`;
···
],
},
},
+
"stale": false,
}
`;
+2
exchanges/refocus/src/refocusExchange.test.ts
···
ExchangeIO,
} from '@urql/core';
import { refocusExchange } from './refocusExchange';
const dispatchDebug = vi.fn();
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
return {
operation: forwardOp,
data: queryOneData,
};
···
ExchangeIO,
} from '@urql/core';
+
import { queryResponse } from '../../../packages/core/src/test-utils';
import { refocusExchange } from './refocusExchange';
const dispatchDebug = vi.fn();
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
return {
+
...queryResponse,
operation: forwardOp,
data: queryOneData,
};
+3
exchanges/request-policy/src/requestPolicyExchange.test.ts
···
ExchangeIO,
} from '@urql/core';
import { requestPolicyExchange } from './requestPolicyExchange';
const dispatchDebug = vi.fn();
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
return {
operation: forwardOp,
data: queryOneData,
};
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
return {
operation: forwardOp,
data: queryOneData,
};
···
ExchangeIO,
} from '@urql/core';
+
import { queryResponse } from '../../../packages/core/src/test-utils';
import { requestPolicyExchange } from './requestPolicyExchange';
const dispatchDebug = vi.fn();
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
return {
+
...queryResponse,
operation: forwardOp,
data: queryOneData,
};
···
const response = vi.fn(
(forwardOp: Operation): OperationResult => {
return {
+
...queryResponse,
operation: forwardOp,
data: queryOneData,
};
+48 -5
packages/core/src/client.test.ts
···
}
}),
map(op => ({
data: op.key,
operation: op,
}))
···
ops$,
filter(op => op.kind !== 'teardown'),
map(op => ({
data: ++countRes,
operation: op,
})),
···
expect(output.length).toBe(3);
expect(output[2]).toHaveProperty('data', 2);
-
expect(output[2]).not.toHaveProperty('stale');
expect(output[2]).toHaveProperty('operation.key', queryOperation.key);
expect(output[2]).toHaveProperty(
'operation.context.requestPolicy',
···
data: 1,
operation: op,
stale: true,
})),
delay(1)
);
···
return pipe(
ops$,
map(op => ({
data: ++i,
operation: op,
})),
···
expect(resultOne).toHaveBeenCalledWith({
data: 1,
operation: queryOperation,
});
pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo));
···
expect(resultTwo).toHaveBeenCalledWith({
data: 1,
operation: queryOperation,
});
vi.advanceTimersByTime(1);
···
return pipe(
ops$,
map(op => ({
data: ++i,
operation: op,
})),
···
expect(resultOne).toHaveBeenCalledWith({
data: 1,
operation: operationOne,
});
pipe(client.executeRequestOperation(operationTwo), subscribe(resultTwo));
···
data: 1,
operation: operationOne,
stale: true,
});
vi.advanceTimersByTime(1);
···
expect(resultTwo).toHaveBeenCalledWith({
data: 2,
operation: operationTwo,
});
});
···
return pipe(
ops$,
map(op => ({
data: ++i,
operation: op,
})),
···
expect(resultOne).toHaveBeenCalledWith({
data: 1,
operation,
});
pipe(client.executeRequestOperation(operation), subscribe(resultTwo));
···
data: 1,
operation,
stale: true,
});
vi.advanceTimersByTime(1);
···
expect(resultTwo).toHaveBeenCalledWith({
data: 2,
operation,
});
});
···
ops$,
filter(op => op.kind !== 'teardown'),
map(op => ({
data: ++i,
operation: op,
})),
···
expect(resultOne).toHaveBeenCalledWith({
data: 1,
operation: queryOperation,
});
subscription.unsubscribe();
···
expect(resultTwo).toHaveBeenCalledWith({
data: 2,
operation: queryOperation,
});
});
···
map(op => ({
data: ++i,
operation: op,
})),
take(1)
);
···
data: 1,
operation,
stale: true,
});
});
···
const exchange: Exchange = () => ops$ => {
return pipe(
ops$,
-
map(op => ({ data: 1, operation: op })),
filter(() => false)
);
};
···
let i = 0;
return pipe(
ops$,
-
map(op => ({ data: ++i, operation: op }))
);
};
···
expect(resultOne).toHaveBeenCalledWith({
data: 1,
operation,
});
pipe(client.executeRequestOperation(operation), subscribe(resultTwo));
···
expect(resultTwo).toHaveBeenCalledWith({
data: 2,
operation,
});
expect(resultOne).toHaveBeenCalledWith({
data: 2,
operation,
});
});
···
const exchange: Exchange = () => ops$ => {
return pipe(
ops$,
-
map(op => ({ stale: true, data: 1, operation: op })),
take(1)
);
};
···
data: 1,
operation: queryOperation,
stale: true,
});
pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo));
···
data: 1,
operation: queryOperation,
stale: true,
});
});
···
const exchange: Exchange = () => ops$ => {
return pipe(
ops$,
-
map(op => ({ stale: true, data: 1, operation: op }))
);
};
···
data: 1,
operation: queryOperation,
stale: true,
});
});
});
···
}
}),
map(op => ({
+
stale: false,
+
hasNext: false,
data: op.key,
operation: op,
}))
···
ops$,
filter(op => op.kind !== 'teardown'),
map(op => ({
+
hasNext: false,
+
stale: false,
data: ++countRes,
operation: op,
})),
···
expect(output.length).toBe(3);
expect(output[2]).toHaveProperty('data', 2);
+
expect(output[2]).toHaveProperty('stale', false);
expect(output[2]).toHaveProperty('operation.key', queryOperation.key);
expect(output[2]).toHaveProperty(
'operation.context.requestPolicy',
···
data: 1,
operation: op,
stale: true,
+
hasNext: false,
})),
delay(1)
);
···
return pipe(
ops$,
map(op => ({
+
hasNext: false,
+
stale: false,
data: ++i,
operation: op,
})),
···
expect(resultOne).toHaveBeenCalledWith({
data: 1,
operation: queryOperation,
+
stale: false,
+
hasNext: false,
});
pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo));
···
expect(resultTwo).toHaveBeenCalledWith({
data: 1,
operation: queryOperation,
+
stale: false,
+
hasNext: false,
});
vi.advanceTimersByTime(1);
···
return pipe(
ops$,
map(op => ({
+
hasNext: false,
+
stale: false,
data: ++i,
operation: op,
})),
···
expect(resultOne).toHaveBeenCalledWith({
data: 1,
operation: operationOne,
+
hasNext: false,
+
stale: false,
});
pipe(client.executeRequestOperation(operationTwo), subscribe(resultTwo));
···
data: 1,
operation: operationOne,
stale: true,
+
hasNext: false,
});
vi.advanceTimersByTime(1);
···
expect(resultTwo).toHaveBeenCalledWith({
data: 2,
operation: operationTwo,
+
stale: false,
+
hasNext: false,
});
});
···
return pipe(
ops$,
map(op => ({
+
hasNext: false,
+
stale: false,
data: ++i,
operation: op,
})),
···
expect(resultOne).toHaveBeenCalledWith({
data: 1,
operation,
+
stale: false,
+
hasNext: false,
});
pipe(client.executeRequestOperation(operation), subscribe(resultTwo));
···
data: 1,
operation,
stale: true,
+
hasNext: false,
});
vi.advanceTimersByTime(1);
···
expect(resultTwo).toHaveBeenCalledWith({
data: 2,
operation,
+
stale: false,
+
hasNext: false,
});
});
···
ops$,
filter(op => op.kind !== 'teardown'),
map(op => ({
+
hasNext: false,
+
stale: false,
data: ++i,
operation: op,
})),
···
expect(resultOne).toHaveBeenCalledWith({
data: 1,
operation: queryOperation,
+
hasNext: false,
+
stale: false,
});
subscription.unsubscribe();
···
expect(resultTwo).toHaveBeenCalledWith({
data: 2,
operation: queryOperation,
+
stale: false,
+
hasNext: false,
});
});
···
map(op => ({
data: ++i,
operation: op,
+
hasNext: false,
+
stale: false,
})),
take(1)
);
···
data: 1,
operation,
stale: true,
+
hasNext: false,
});
});
···
const exchange: Exchange = () => ops$ => {
return pipe(
ops$,
+
map(op => ({ hasNext: false, stale: false, data: 1, operation: op })),
filter(() => false)
);
};
···
let i = 0;
return pipe(
ops$,
+
map(op => ({ hasNext: false, stale: false, data: ++i, operation: op }))
);
};
···
expect(resultOne).toHaveBeenCalledWith({
data: 1,
operation,
+
hasNext: false,
+
stale: false,
});
pipe(client.executeRequestOperation(operation), subscribe(resultTwo));
···
expect(resultTwo).toHaveBeenCalledWith({
data: 2,
operation,
+
hasNext: false,
+
stale: false,
});
expect(resultOne).toHaveBeenCalledWith({
data: 2,
operation,
+
hasNext: false,
+
stale: false,
});
});
···
const exchange: Exchange = () => ops$ => {
return pipe(
ops$,
+
map(op => ({ hasNext: false, stale: true, data: 1, operation: op })),
take(1)
);
};
···
data: 1,
operation: queryOperation,
stale: true,
+
hasNext: false,
});
pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo));
···
data: 1,
operation: queryOperation,
stale: true,
+
hasNext: false,
});
});
···
const exchange: Exchange = () => ops$ => {
return pipe(
ops$,
+
map(op => ({ hasNext: false, stale: true, data: 1, operation: op }))
);
};
···
data: 1,
operation: queryOperation,
stale: true,
+
hasNext: false,
});
});
});
+5
packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
···
"data": undefined,
"error": [CombinedError: [Network] No Content],
"extensions": undefined,
"operation": {
"context": {
"fetchOptions": {
···
"name": "Clara",
},
},
}
`;
···
"data": undefined,
"error": [CombinedError: [Network] No Content],
"extensions": undefined,
"operation": {
"context": {
"fetchOptions": [MockFunction spy] {
···
"name": "Clara",
},
},
}
`;
···
"name": "Clara",
},
},
}
`;
···
"data": undefined,
"error": [CombinedError: [Network] No Content],
"extensions": undefined,
+
"hasNext": false,
"operation": {
"context": {
"fetchOptions": {
···
"name": "Clara",
},
},
+
"stale": false,
}
`;
···
"data": undefined,
"error": [CombinedError: [Network] No Content],
"extensions": undefined,
+
"hasNext": false,
"operation": {
"context": {
"fetchOptions": [MockFunction spy] {
···
"name": "Clara",
},
},
+
"stale": false,
}
`;
···
"name": "Clara",
},
},
+
"stale": false,
}
`;
+1
packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap
···
"user": "colin",
},
},
}
`;
···
"user": "colin",
},
},
+
"stale": false,
}
`;
+9 -6
packages/core/src/exchanges/cache.ts
···
}),
});
-
const result: OperationResult = {
-
...cachedResult,
-
operation: addMetadata(operation, {
-
cacheOutcome: cachedResult ? 'hit' : 'miss',
-
}),
-
};
if (operation.context.requestPolicy === 'cache-and-network') {
result.stale = true;
···
}),
});
+
let result: OperationResult = cachedResult!;
+
if (process.env.NODE_ENV !== 'production') {
+
result = {
+
...result,
+
operation: addMetadata(operation, {
+
cacheOutcome: cachedResult ? 'hit' : 'miss',
+
}),
+
};
+
}
if (operation.context.requestPolicy === 'cache-and-network') {
result.stale = true;
+2 -2
packages/core/src/exchanges/map.test.ts
···
import { vi, expect, describe, it } from 'vitest';
import { Client } from '../client';
-
import { queryOperation } from '../test-utils';
import { Operation } from '../types';
import { mapExchange } from './map';
···
forward: op$ =>
pipe(
op$,
-
map((operation: Operation) => ({ operation }))
),
client: {} as Client,
dispatchDebug: () => null,
···
import { vi, expect, describe, it } from 'vitest';
import { Client } from '../client';
+
import { queryResponse, queryOperation } from '../test-utils';
import { Operation } from '../types';
import { mapExchange } from './map';
···
forward: op$ =>
pipe(
op$,
+
map((operation: Operation) => ({ ...queryResponse, operation }))
),
client: {} as Client,
dispatchDebug: () => null,
+11 -6
packages/core/src/exchanges/ssr.test.ts
···
[queryOperation.key]: {
data: serializedQueryResponse.data,
error: undefined,
},
});
});
it('serializes query results quickly', () => {
-
const queryResponse: OperationResult = {
operation: queryOperation,
data: {
user: {
···
};
const serializedQueryResponse = {
-
...queryResponse,
-
data: JSON.stringify(queryResponse.data),
};
-
output.mockReturnValueOnce(queryResponse);
const ssr = ssrExchange();
const { source: ops$, next } = input;
···
publish(exchange);
next(queryOperation);
-
queryResponse.data.user.name = 'Not Clive';
const data = ssr.extractData();
expect(Object.keys(data)).toEqual(['' + queryOperation.key]);
···
[queryOperation.key]: {
data: serializedQueryResponse.data,
error: undefined,
},
});
});
···
],
networkError: undefined,
},
},
});
});
···
[queryOperation.key]: {
data: '{"user":{"name":"Clive"}}',
extensions: '{"foo":"bar"}',
},
});
});
···
isClient: true,
initialState: {
[queryOperation.key]: {
-
hasNext: true,
...(serializedQueryResponse as any),
},
},
});
···
[queryOperation.key]: {
data: serializedQueryResponse.data,
error: undefined,
+
hasNext: false,
},
});
});
it('serializes query results quickly', () => {
+
const result: OperationResult = {
+
...queryResponse,
operation: queryOperation,
data: {
user: {
···
};
const serializedQueryResponse = {
+
...result,
+
data: JSON.stringify(result.data),
};
+
output.mockReturnValueOnce(result);
const ssr = ssrExchange();
const { source: ops$, next } = input;
···
publish(exchange);
next(queryOperation);
+
result.data.user.name = 'Not Clive';
const data = ssr.extractData();
expect(Object.keys(data)).toEqual(['' + queryOperation.key]);
···
[queryOperation.key]: {
data: serializedQueryResponse.data,
error: undefined,
+
hasNext: false,
},
});
});
···
],
networkError: undefined,
},
+
hasNext: false,
},
});
});
···
[queryOperation.key]: {
data: '{"user":{"name":"Clive"}}',
extensions: '{"foo":"bar"}',
+
hasNext: false,
},
});
});
···
isClient: true,
initialState: {
[queryOperation.key]: {
...(serializedQueryResponse as any),
+
hasNext: true,
},
},
});
+8
packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
···
"data": undefined,
"error": [CombinedError: [Network] Forbidden],
"extensions": undefined,
"operation": {
"context": {
"fetchOptions": {
···
"name": "Clara",
},
},
}
`;
···
"data": undefined,
"error": [CombinedError: [Network] Forbidden],
"extensions": undefined,
"operation": {
"context": {
"fetchOptions": {
···
"name": "Clara",
},
},
}
`;
···
"data": undefined,
"error": [CombinedError: [Network] Forbidden],
"extensions": undefined,
"operation": {
"context": {
"fetchOptions": {
···
"name": "Clara",
},
},
}
`;
···
"name": "Clara",
},
},
}
`;
···
"name": "Clara",
},
},
}
`;
···
"data": undefined,
"error": [CombinedError: [Network] Forbidden],
"extensions": undefined,
+
"hasNext": false,
"operation": {
"context": {
"fetchOptions": {
···
"name": "Clara",
},
},
+
"stale": false,
}
`;
···
"data": undefined,
"error": [CombinedError: [Network] Forbidden],
"extensions": undefined,
+
"hasNext": false,
"operation": {
"context": {
"fetchOptions": {
···
"name": "Clara",
},
},
+
"stale": false,
}
`;
···
"data": undefined,
"error": [CombinedError: [Network] Forbidden],
"extensions": undefined,
+
"hasNext": false,
"operation": {
"context": {
"fetchOptions": {
···
"name": "Clara",
},
},
+
"stale": false,
}
`;
···
"name": "Clara",
},
},
+
"stale": false,
}
`;
···
"name": "Clara",
},
},
+
"stale": false,
}
`;
+6
packages/core/src/test-utils/samples.ts
···
export const undefinedQueryResponse: OperationResult = {
operation: queryOperation,
};
export const queryResponse: OperationResult = {
···
name: 'Clive',
},
},
};
export const mutationResponse: OperationResult = {
operation: mutationOperation,
data: {},
};
export const subscriptionResult: ExecutionResult = {
···
export const undefinedQueryResponse: OperationResult = {
operation: queryOperation,
+
stale: false,
+
hasNext: false,
};
export const queryResponse: OperationResult = {
···
name: 'Clive',
},
},
+
stale: false,
+
hasNext: false,
};
export const mutationResponse: OperationResult = {
operation: mutationOperation,
data: {},
+
stale: false,
+
hasNext: false,
};
export const subscriptionResult: ExecutionResult = {
+2 -2
packages/core/src/types.ts
···
* Most commonly, this flag is set for a cached result when the operation is executed using the
* `cache-and-network` {@link RequestPolicy}.
*/
-
stale?: boolean;
/** Indicates that the GraphQL response is streamed and updated results will follow.
*
* @remarks
···
*
* For GraphQL subscriptions, this flag will always be set to `true`.
*/
-
hasNext?: boolean;
}
/** The input parameters a `Client` passes to an `Exchange` when it's created.
···
* Most commonly, this flag is set for a cached result when the operation is executed using the
* `cache-and-network` {@link RequestPolicy}.
*/
+
stale: boolean;
/** Indicates that the GraphQL response is streamed and updated results will follow.
*
* @remarks
···
*
* For GraphQL subscriptions, this flag will always be set to `true`.
*/
+
hasNext: boolean;
}
/** The input parameters a `Client` passes to an `Exchange` when it's created.
+16
packages/core/src/utils/result.test.ts
···
},
],
},
};
const merged = mergeResultPatch(prevResult, {
···
},
],
},
};
const patch = { __typename: 'Child' };
···
__typename: 'Query',
item: undefined,
},
};
const merged = mergeResultPatch(prevResult, {
···
__typename: 'Query',
items: [{ __typename: 'Item' }],
},
};
const patch = { __typename: 'Item' };
···
__typename: 'Query',
items: [{ __typename: 'Item' }],
},
};
const merged = mergeResultPatch(prevResult, {
···
extensions: {
base: true,
},
};
const merged = mergeResultPatch(prevResult, {
···
extensions: {
base: true,
},
};
const merged = mergeResultPatch(prevResult, {
···
},
],
},
};
const patch = { __typename: 'Child' };
···
},
],
},
+
stale: false,
+
hasNext: true,
};
const merged = mergeResultPatch(prevResult, {
···
},
],
},
+
stale: false,
+
hasNext: true,
};
const patch = { __typename: 'Child' };
···
__typename: 'Query',
item: undefined,
},
+
stale: false,
+
hasNext: true,
};
const merged = mergeResultPatch(prevResult, {
···
__typename: 'Query',
items: [{ __typename: 'Item' }],
},
+
stale: false,
+
hasNext: true,
};
const patch = { __typename: 'Item' };
···
__typename: 'Query',
items: [{ __typename: 'Item' }],
},
+
stale: false,
+
hasNext: true,
};
const merged = mergeResultPatch(prevResult, {
···
extensions: {
base: true,
},
+
stale: false,
+
hasNext: true,
};
const merged = mergeResultPatch(prevResult, {
···
extensions: {
base: true,
},
+
stale: false,
+
hasNext: true,
};
const merged = mergeResultPatch(prevResult, {
···
},
],
},
+
stale: false,
+
hasNext: true,
};
const patch = { __typename: 'Child' };
+4
packages/core/src/utils/result.ts
···
: undefined,
extensions: result.extensions ? { ...result.extensions } : undefined,
hasNext: result.hasNext == null ? defaultHasNext : result.hasNext,
};
};
···
: undefined,
extensions: hasExtensions ? extensions : undefined,
hasNext: !!nextResult.hasNext,
};
};
···
response,
}),
extensions: undefined,
});
···
: undefined,
extensions: result.extensions ? { ...result.extensions } : undefined,
hasNext: result.hasNext == null ? defaultHasNext : result.hasNext,
+
stale: false,
};
};
···
: undefined,
extensions: hasExtensions ? extensions : undefined,
hasNext: !!nextResult.hasNext,
+
stale: false,
};
};
···
response,
}),
extensions: undefined,
+
hasNext: false,
+
stale: false,
});
+2
packages/react-urql/src/test-utils/ssr.test.tsx
···
name: 'Clive',
},
},
};
const url = 'https://hostname.com';
···
name: 'Clive',
},
},
+
stale: false,
+
hasNext: false,
};
const url = 'https://hostname.com';
+4 -2
packages/svelte-urql/src/common.ts
···
make(observer => store$.subscribe(observer.next));
export const initialResult = {
fetching: false,
-
stale: false,
-
error: undefined,
data: undefined,
extensions: undefined,
};
export interface Pausable {
···
make(observer => store$.subscribe(observer.next));
export const initialResult = {
+
operation: undefined,
fetching: false,
data: undefined,
+
error: undefined,
extensions: undefined,
+
hasNext: false,
+
stale: false,
};
export interface Pausable {