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

feat(core): Reuse stringifyDocument and cached prints across @urql/core (#2847)

authored by kitten.sh and committed by

GitHub ece64fcf fbd254ba

+351 -203
+8
.changeset/quiet-mugs-travel.md
··· 1 + --- 2 + '@urql/core': patch 3 + --- 4 + 5 + Reuse output of `stringifyDocument` in place of repeated `print`. This will mean that we now prevent calling `print` repeatedly for identical operations and are instead only reusing the result once. 6 + 7 + This change has a subtle consequence of our internals. Operation keys will change due to this 8 + refactor and we will no longer sanitise strip newlines from queries that `@urql/core` has printed.
+41 -20
exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
··· 16 16 "key": 2, 17 17 "kind": "query", 18 18 "query": { 19 - "__key": 3521976120, 19 + "__key": -2395444236, 20 20 "definitions": [ 21 21 { 22 22 "directives": [], ··· 119 119 ], 120 120 "kind": "Document", 121 121 "loc": { 122 - "end": 86, 122 + "end": 92, 123 123 "source": { 124 - "body": "# getUser 125 - query getUser($name: String) { user(name: $name) { id firstName lastName } }", 124 + "body": "query getUser($name: String) { 125 + user(name: $name) { 126 + id 127 + firstName 128 + lastName 129 + } 130 + }", 126 131 "locationOffset": { 127 132 "column": 1, 128 133 "line": 1, ··· 165 170 "key": 2, 166 171 "kind": "query", 167 172 "query": { 168 - "__key": 3521976120, 173 + "__key": -2395444236, 169 174 "definitions": [ 170 175 { 171 176 "directives": [], ··· 268 273 ], 269 274 "kind": "Document", 270 275 "loc": { 271 - "end": 86, 276 + "end": 92, 272 277 "source": { 273 - "body": "# getUser 274 - query getUser($name: String) { user(name: $name) { id firstName lastName } }", 278 + "body": "query getUser($name: String) { 279 + user(name: $name) { 280 + id 281 + firstName 282 + lastName 283 + } 284 + }", 275 285 "locationOffset": { 276 286 "column": 1, 277 287 "line": 1, ··· 317 327 "key": 2, 318 328 "kind": "query", 319 329 "query": { 320 - "__key": 3521976120, 330 + "__key": -2395444236, 321 331 "definitions": [ 322 332 { 323 333 "directives": [], ··· 420 430 ], 421 431 "kind": "Document", 422 432 "loc": { 423 - "end": 86, 433 + "end": 92, 424 434 "source": { 425 - "body": "# getUser 426 - query getUser($name: String) { user(name: $name) { id firstName lastName } }", 435 + "body": "query getUser($name: String) { 436 + user(name: $name) { 437 + id 438 + firstName 439 + lastName 440 + } 441 + }", 427 442 "locationOffset": { 428 443 "column": 1, 429 444 "line": 1, ··· 471 486 "key": 3, 472 487 "kind": "mutation", 473 488 "query": { 474 - "__key": 4034972436, 489 + "__key": 8029062428, 475 490 "definitions": [ 476 491 { 477 492 "directives": [], ··· 552 567 ], 553 568 "kind": "Document", 554 569 "loc": { 555 - "end": 125, 570 + "end": 110, 556 571 "source": { 557 - "body": "# uploadProfilePicture 558 - mutation uploadProfilePicture($picture: File) { uploadProfilePicture(picture: $picture) { location } }", 572 + "body": "mutation uploadProfilePicture($picture: File) { 573 + uploadProfilePicture(picture: $picture) { 574 + location 575 + } 576 + }", 559 577 "locationOffset": { 560 578 "column": 1, 561 579 "line": 1, ··· 609 627 "key": 3, 610 628 "kind": "mutation", 611 629 "query": { 612 - "__key": 2033658603, 630 + "__key": -6039055341, 613 631 "definitions": [ 614 632 { 615 633 "directives": [], ··· 693 711 ], 694 712 "kind": "Document", 695 713 "loc": { 696 - "end": 132, 714 + "end": 116, 697 715 "source": { 698 - "body": "# uploadProfilePictures 699 - mutation uploadProfilePictures($pictures: [File]) { uploadProfilePicture(pictures: $pictures) { location } }", 716 + "body": "mutation uploadProfilePictures($pictures: [File]) { 717 + uploadProfilePicture(pictures: $pictures) { 718 + location 719 + } 720 + }", 700 721 "locationOffset": { 701 722 "column": 1, 702 723 "line": 1,
+27 -12
packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
··· 16 16 "key": 2, 17 17 "kind": "query", 18 18 "query": { 19 - "__key": 3521976120, 19 + "__key": -2395444236, 20 20 "definitions": [ 21 21 { 22 22 "directives": [], ··· 119 119 ], 120 120 "kind": "Document", 121 121 "loc": { 122 - "end": 86, 122 + "end": 92, 123 123 "source": { 124 - "body": "# getUser 125 - query getUser($name: String) { user(name: $name) { id firstName lastName } }", 124 + "body": "query getUser($name: String) { 125 + user(name: $name) { 126 + id 127 + firstName 128 + lastName 129 + } 130 + }", 126 131 "locationOffset": { 127 132 "column": 1, 128 133 "line": 1, ··· 165 170 "key": 2, 166 171 "kind": "query", 167 172 "query": { 168 - "__key": 3521976120, 173 + "__key": -2395444236, 169 174 "definitions": [ 170 175 { 171 176 "directives": [], ··· 268 273 ], 269 274 "kind": "Document", 270 275 "loc": { 271 - "end": 86, 276 + "end": 92, 272 277 "source": { 273 - "body": "# getUser 274 - query getUser($name: String) { user(name: $name) { id firstName lastName } }", 278 + "body": "query getUser($name: String) { 279 + user(name: $name) { 280 + id 281 + firstName 282 + lastName 283 + } 284 + }", 275 285 "locationOffset": { 276 286 "column": 1, 277 287 "line": 1, ··· 317 327 "key": 2, 318 328 "kind": "query", 319 329 "query": { 320 - "__key": 3521976120, 330 + "__key": -2395444236, 321 331 "definitions": [ 322 332 { 323 333 "directives": [], ··· 420 430 ], 421 431 "kind": "Document", 422 432 "loc": { 423 - "end": 86, 433 + "end": 92, 424 434 "source": { 425 - "body": "# getUser 426 - query getUser($name: String) { user(name: $name) { id firstName lastName } }", 435 + "body": "query getUser($name: String) { 436 + user(name: $name) { 437 + id 438 + firstName 439 + lastName 440 + } 441 + }", 427 442 "locationOffset": { 428 443 "column": 1, 429 444 "line": 1,
+7 -4
packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap
··· 17 17 "key": 4, 18 18 "kind": "subscription", 19 19 "query": { 20 - "__key": 2088253569, 20 + "__key": 7623921801, 21 21 "definitions": [ 22 22 { 23 23 "directives": [], ··· 98 98 ], 99 99 "kind": "Document", 100 100 "loc": { 101 - "end": 92, 101 + "end": 82, 102 102 "source": { 103 - "body": "# subscribeToUser 104 - subscription subscribeToUser($user: String) { user(user: $user) { name } }", 103 + "body": "subscription subscribeToUser($user: String) { 104 + user(user: $user) { 105 + name 106 + } 107 + }", 105 108 "locationOffset": { 106 109 "column": 1, 107 110 "line": 1,
+5 -2
packages/core/src/exchanges/subscription.test.ts
··· 1 - import { print } from 'graphql'; 2 1 import { vi, expect, it } from 'vitest'; 2 + 3 3 import { 4 4 empty, 5 5 publish, ··· 12 12 13 13 import { Client } from '../client'; 14 14 import { subscriptionOperation, subscriptionResult } from '../test-utils'; 15 + import { stringifyDocument } from '../utils'; 15 16 import { OperationResult } from '../types'; 16 17 import { subscriptionExchange, SubscriptionForwarder } from './subscription'; 17 18 ··· 24 25 25 26 const unsubscribe = vi.fn(); 26 27 const forwardSubscription: SubscriptionForwarder = operation => { 27 - expect(operation.query).toBe(print(subscriptionOperation.query)); 28 + expect(operation.query).toBe( 29 + stringifyDocument(subscriptionOperation.query) 30 + ); 28 31 expect(operation.variables).toBe(subscriptionOperation.variables); 29 32 expect(operation.context).toEqual(subscriptionOperation.context); 30 33
+7 -4
packages/core/src/exchanges/subscription.ts
··· 1 - import { print } from 'graphql'; 2 - 3 1 import { 4 2 filter, 5 3 make, ··· 12 10 takeUntil, 13 11 } from 'wonka'; 14 12 15 - import { makeResult, makeErrorResult, makeOperation } from '../utils'; 13 + import { 14 + stringifyDocument, 15 + makeResult, 16 + makeErrorResult, 17 + makeOperation, 18 + } from '../utils'; 16 19 17 20 import { 18 21 Exchange, ··· 70 73 // This excludes the query's name as a field although subscription-transport-ws does accept it since it's optional 71 74 const observableish = forwardSubscription({ 72 75 key: operation.key.toString(36), 73 - query: print(operation.query), 76 + query: stringifyDocument(operation.query), 74 77 variables: operation.variables!, 75 78 context: { ...operation.context }, 76 79 });
+2 -2
packages/core/src/gql.test.ts
··· 23 23 parse('{ gql testing }', { noLocation: true }).definitions 24 24 ); 25 25 26 - expect(doc).toBe(keyDocument('{ gql testing }')); 26 + expect(doc).toBe(keyDocument('{\n gql\n testing\n}')); 27 27 expect(doc.loc).toEqual({ 28 28 start: 0, 29 - end: 15, 29 + end: 19, 30 30 source: expect.anything(), 31 31 }); 32 32 });
+45 -20
packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
··· 16 16 "key": 2, 17 17 "kind": "query", 18 18 "query": { 19 - "__key": 3521976120, 19 + "__key": -2395444236, 20 20 "definitions": [ 21 21 { 22 22 "directives": [], ··· 119 119 ], 120 120 "kind": "Document", 121 121 "loc": { 122 - "end": 86, 122 + "end": 92, 123 123 "source": { 124 - "body": "# getUser 125 - query getUser($name: String) { user(name: $name) { id firstName lastName } }", 124 + "body": "query getUser($name: String) { 125 + user(name: $name) { 126 + id 127 + firstName 128 + lastName 129 + } 130 + }", 126 131 "locationOffset": { 127 132 "column": 1, 128 133 "line": 1, ··· 155 160 "key": 2, 156 161 "kind": "query", 157 162 "query": { 158 - "__key": 3521976120, 163 + "__key": -2395444236, 159 164 "definitions": [ 160 165 { 161 166 "directives": [], ··· 258 263 ], 259 264 "kind": "Document", 260 265 "loc": { 261 - "end": 86, 266 + "end": 92, 262 267 "source": { 263 - "body": "# getUser 264 - query getUser($name: String) { user(name: $name) { id firstName lastName } }", 268 + "body": "query getUser($name: String) { 269 + user(name: $name) { 270 + id 271 + firstName 272 + lastName 273 + } 274 + }", 265 275 "locationOffset": { 266 276 "column": 1, 267 277 "line": 1, ··· 294 304 "key": 2, 295 305 "kind": "query", 296 306 "query": { 297 - "__key": 3521976120, 307 + "__key": -2395444236, 298 308 "definitions": [ 299 309 { 300 310 "directives": [], ··· 397 407 ], 398 408 "kind": "Document", 399 409 "loc": { 400 - "end": 86, 410 + "end": 92, 401 411 "source": { 402 - "body": "# getUser 403 - query getUser($name: String) { user(name: $name) { id firstName lastName } }", 412 + "body": "query getUser($name: String) { 413 + user(name: $name) { 414 + id 415 + firstName 416 + lastName 417 + } 418 + }", 404 419 "locationOffset": { 405 420 "column": 1, 406 421 "line": 1, ··· 438 453 "key": 2, 439 454 "kind": "query", 440 455 "query": { 441 - "__key": 3521976120, 456 + "__key": -2395444236, 442 457 "definitions": [ 443 458 { 444 459 "directives": [], ··· 541 556 ], 542 557 "kind": "Document", 543 558 "loc": { 544 - "end": 86, 559 + "end": 92, 545 560 "source": { 546 - "body": "# getUser 547 - query getUser($name: String) { user(name: $name) { id firstName lastName } }", 561 + "body": "query getUser($name: String) { 562 + user(name: $name) { 563 + id 564 + firstName 565 + lastName 566 + } 567 + }", 548 568 "locationOffset": { 549 569 "column": 1, 550 570 "line": 1, ··· 611 631 "key": 2, 612 632 "kind": "query", 613 633 "query": { 614 - "__key": 3521976120, 634 + "__key": -2395444236, 615 635 "definitions": [ 616 636 { 617 637 "directives": [], ··· 714 734 ], 715 735 "kind": "Document", 716 736 "loc": { 717 - "end": 86, 737 + "end": 92, 718 738 "source": { 719 - "body": "# getUser 720 - query getUser($name: String) { user(name: $name) { id firstName lastName } }", 739 + "body": "query getUser($name: String) { 740 + user(name: $name) { 741 + id 742 + firstName 743 + lastName 744 + } 745 + }", 721 746 "locationOffset": { 722 747 "column": 1, 723 748 "line": 1,
+7 -5
packages/core/src/internal/fetchOptions.ts
··· 1 - import { print } from 'graphql'; 2 - import { getOperationName, stringifyVariables } from '../utils'; 1 + import { 2 + stringifyDocument, 3 + getOperationName, 4 + stringifyVariables, 5 + } from '../utils'; 3 6 import { AnyVariables, GraphQLRequest, Operation } from '../types'; 4 7 5 8 export interface FetchBody { ··· 14 17 Variables extends AnyVariables = AnyVariables 15 18 >(request: Omit<GraphQLRequest<Data, Variables>, 'key'>): FetchBody { 16 19 return { 17 - query: print(request.query), 20 + query: stringifyDocument(request.query), 18 21 operationName: getOperationName(request.query), 19 22 variables: request.variables || undefined, 20 23 extensions: undefined, ··· 32 35 const url = new URL(operation.context.url); 33 36 const search = url.searchParams; 34 37 if (body.operationName) search.set('operationName', body.operationName); 35 - if (body.query) 36 - search.set('query', body.query.replace(/#[^\n\r]+/g, ' ').trim()); 38 + if (body.query) search.set('query', body.query); 37 39 if (body.variables) 38 40 search.set('variables', stringifyVariables(body.variables)); 39 41 if (body.extensions)
+14
packages/core/src/utils/hash.test.ts
··· 1 + import { HashValue, phash } from './hash'; 2 + import { expect, it } from 'vitest'; 3 + 4 + it('hashes given strings', () => { 5 + expect(phash('hello')).toMatchInlineSnapshot('261238937'); 6 + }); 7 + 8 + it('hashes given strings and seeds', () => { 9 + let hash: HashValue; 10 + expect((hash = phash('hello'))).toMatchInlineSnapshot('261238937'); 11 + expect((hash = phash('world', hash))).toMatchInlineSnapshot('-152191'); 12 + expect((hash = phash('!', hash))).toMatchInlineSnapshot('-5022270'); 13 + expect(typeof hash).toBe('number'); 14 + });
+5 -5
packages/core/src/utils/hash.ts
··· 1 + export type HashValue = number & { readonly _opaque: unique symbol }; 2 + 1 3 // When we have separate strings it's useful to run a progressive 2 4 // version of djb2 where we pretend that we're still looping over 3 5 // the same string 4 - export const phash = (h: number, x: string): number => { 6 + export const phash = (x: string, seed?: HashValue): HashValue => { 7 + let h = typeof seed === 'number' ? seed | 0 : 5381; 5 8 for (let i = 0, l = x.length | 0; i < l; i++) 6 9 h = (h << 5) + h + x.charCodeAt(i); 7 - return h | 0; 10 + return h as HashValue; 8 11 }; 9 - 10 - // This is a djb2 hashing function 11 - export const hash = (x: string): number => phash(5381 | 0, x) >>> 0;
+109 -82
packages/core/src/utils/request.test.ts
··· 1 - import { vi, expect, it, describe } from 'vitest'; 2 - 3 - vi.mock('./hash', async () => { 4 - const hash = await vi.importActual<typeof import('./hash')>('./hash'); 5 - return { 6 - ...hash, 7 - phash: (x: number) => x, 8 - }; 9 - }); 1 + import { expect, it, describe } from 'vitest'; 10 2 11 3 import { parse, print } from 'graphql'; 12 4 import { gql } from '../gql'; 13 5 import { createRequest, stringifyDocument } from './request'; 14 - 15 - it('should hash identical queries identically', () => { 16 - const reqA = createRequest('{ test }', undefined); 17 - const reqB = createRequest('{ test }', undefined); 18 - expect(reqA.key).toBe(reqB.key); 19 - }); 6 + import { formatDocument } from './typenames'; 20 7 21 - it('should hash identical DocumentNodes identically', () => { 22 - const reqA = createRequest(parse('{ testB }'), undefined); 23 - const reqB = createRequest(parse('{ testB }'), undefined); 24 - expect(reqA.key).toBe(reqB.key); 25 - expect(reqA.query).toBe(reqB.query); 26 - }); 8 + describe('createRequest', () => { 9 + it('should hash identical queries identically', () => { 10 + const reqA = createRequest('{ test }', undefined); 11 + const reqB = createRequest('{ test }', undefined); 12 + expect(reqA.key).toBe(reqB.key); 13 + }); 27 14 28 - it('should use the hash from a key if available', () => { 29 - const doc = parse('{ testC }'); 30 - (doc as any).__key = 1234; 31 - const req = createRequest(doc, undefined); 32 - expect(req.key).toBe(1234); 33 - }); 15 + it('should hash identical queries identically', () => { 16 + const reqA = createRequest('{ test }', undefined); 17 + const reqB = createRequest('{ test }', undefined); 18 + expect(reqA.key).toBe(reqB.key); 19 + }); 34 20 35 - it('should hash DocumentNodes and strings identically', () => { 36 - const docA = parse('{ field }'); 37 - const docB = print(docA).replace(/\s/g, ' '); 38 - const reqA = createRequest(docA, undefined); 39 - const reqB = createRequest(docB, undefined); 40 - expect(reqA.key).toBe(reqB.key); 41 - expect(reqA.query).toBe(reqB.query); 42 - }); 21 + it('should hash identical DocumentNodes identically', () => { 22 + const reqA = createRequest(parse('{ testB }'), undefined); 23 + const reqB = createRequest(parse('{ testB }'), undefined); 24 + expect(reqA.key).toBe(reqB.key); 25 + expect(reqA.query).toBe(reqB.query); 26 + }); 43 27 44 - it('should hash graphql-tag documents correctly', () => { 45 - const doc = gql` 46 - { 47 - testD 48 - } 49 - `; 50 - createRequest(doc, undefined); 51 - expect((doc as any).__key).not.toBe(undefined); 52 - }); 28 + it('should use the hash from a key if available', () => { 29 + const doc = parse('{ testC }'); 30 + (doc as any).__key = 1234; 31 + const req = createRequest(doc, undefined); 32 + expect(req.key).toBe(1234); 33 + }); 53 34 54 - it('should return a valid query object', () => { 55 - const doc = gql` 56 - { 57 - testE 58 - } 59 - `; 60 - const val = createRequest(doc, undefined); 35 + it('should hash DocumentNodes and strings identically', () => { 36 + const docA = parse('{ field }'); 37 + const docB = print(docA); 38 + const reqA = createRequest(docA, undefined); 39 + const reqB = createRequest(docB, undefined); 40 + expect(reqA.key).toBe(reqB.key); 41 + expect(reqA.query).toBe(reqB.query); 42 + }); 61 43 62 - expect(val).toMatchObject({ 63 - key: expect.any(Number), 64 - query: expect.any(Object), 65 - variables: {}, 44 + it('should hash graphql-tag documents correctly', () => { 45 + const doc = gql` 46 + { 47 + testD 48 + } 49 + `; 50 + createRequest(doc, undefined); 51 + expect((doc as any).__key).not.toBe(undefined); 66 52 }); 67 - }); 68 53 69 - it('should return a valid query object with variables', () => { 70 - const doc = print( 71 - gql` 54 + it('should return a valid query object', () => { 55 + const doc = gql` 72 56 { 73 - testF 57 + testE 74 58 } 75 - ` 76 - ); 77 - const val = createRequest(doc, { test: 5 }); 59 + `; 60 + const val = createRequest(doc, undefined); 78 61 79 - expect(print(val.query)).toBe(doc); 80 - expect(val).toMatchObject({ 81 - key: expect.any(Number), 82 - query: expect.any(Object), 83 - variables: { test: 5 }, 62 + expect(val).toMatchObject({ 63 + key: expect.any(Number), 64 + query: expect.any(Object), 65 + variables: {}, 66 + }); 67 + }); 68 + 69 + it('should return a valid query object with variables', () => { 70 + const doc = print( 71 + gql` 72 + { 73 + testF 74 + } 75 + ` 76 + ); 77 + const val = createRequest(doc, { test: 5 }); 78 + 79 + expect(print(val.query)).toBe(doc); 80 + expect(val).toMatchObject({ 81 + key: expect.any(Number), 82 + query: expect.any(Object), 83 + variables: { test: 5 }, 84 + }); 84 85 }); 85 86 }); 86 87 87 - describe('stringifyDocument (internal API)', () => { 88 + describe('stringifyDocument ', () => { 89 + it('should reprint formatted documents', () => { 90 + const doc = parse('{ test { field } }'); 91 + const formatted = formatDocument(doc); 92 + expect(stringifyDocument(formatted)).toBe(print(formatted)); 93 + }); 94 + 88 95 it('should remove comments', () => { 89 96 const doc = ` 90 97 { #query ··· 92 99 test 93 100 } 94 101 `; 95 - expect(stringifyDocument(createRequest(doc, undefined).query)).toBe( 96 - '{ test }' 97 - ); 102 + expect(stringifyDocument(createRequest(doc, undefined).query)) 103 + .toMatchInlineSnapshot(` 104 + "{ 105 + test 106 + }" 107 + `); 98 108 }); 99 109 100 110 it('should remove duplicate spaces', () => { ··· 103 113 abc ,, test 104 114 } 105 115 `; 106 - expect(stringifyDocument(createRequest(doc, undefined).query)).toBe( 107 - '{ abc test }' 108 - ); 116 + expect(stringifyDocument(createRequest(doc, undefined).query)) 117 + .toMatchInlineSnapshot(` 118 + "{ 119 + abc 120 + test 121 + }" 122 + `); 109 123 }); 110 124 111 125 it('should not sanitize within strings', () => { ··· 114 128 field(arg: "test #1") 115 129 } 116 130 `; 117 - expect(stringifyDocument(createRequest(doc, undefined).query)).toBe( 118 - '{ field(arg:"test #1") }' 119 - ); 131 + expect(stringifyDocument(createRequest(doc, undefined).query)) 132 + .toMatchInlineSnapshot(` 133 + "{ 134 + field(arg: 135 + 136 + \\"test #1\\") 137 + }" 138 + `); 120 139 }); 121 140 122 141 it('should not sanitize within block strings', () => { ··· 125 144 field( 126 145 arg: """ 127 146 hello 128 - hello 147 + #hello 129 148 """ 130 149 ) 131 150 } 132 151 `; 133 - expect(stringifyDocument(createRequest(doc, undefined).query)).toBe( 134 - '{ field(arg:"""\n hello\n hello\n """) }' 135 - ); 152 + expect(stringifyDocument(createRequest(doc, undefined).query)) 153 + .toMatchInlineSnapshot(` 154 + "{ 155 + field(arg: 156 + 157 + \\"\\"\\" 158 + hello 159 + #hello 160 + \\"\\"\\") 161 + }" 162 + `); 136 163 }); 137 164 });
+52 -44
packages/core/src/utils/request.ts
··· 7 7 print, 8 8 } from 'graphql'; 9 9 10 - import { hash, phash } from './hash'; 10 + import { HashValue, phash } from './hash'; 11 11 import { stringifyVariables } from './stringifyVariables'; 12 12 import { TypedDocumentNode, AnyVariables, GraphQLRequest } from '../types'; 13 13 ··· 16 16 } 17 17 18 18 export interface KeyedDocumentNode extends DocumentNode { 19 - __key: number; 19 + __key: HashValue; 20 20 } 21 21 22 + const SOURCE_NAME = 'gql'; 22 23 const GRAPHQL_STRING_RE = /("{3}[\s\S]*"{3}|"(?:\\.|[^"])*")/g; 23 - const REPLACE_CHAR_RE = /([\s,]|#[^\n\r]+)+/g; 24 + const REPLACE_CHAR_RE = /(#[^\n\r]+)?(?:\n|\r\n?|$)+/g; 24 25 25 26 const replaceOutsideStrings = (str: string, idx: number) => 26 - idx % 2 === 0 ? str.replace(REPLACE_CHAR_RE, ' ').trim() : str; 27 + idx % 2 === 0 ? str.replace(REPLACE_CHAR_RE, '\n') : str; 28 + 29 + const sanitizeDocument = (node: string): string => 30 + node.split(GRAPHQL_STRING_RE).map(replaceOutsideStrings).join('').trim(); 27 31 28 32 export const stringifyDocument = ( 29 33 node: string | DefinitionNode | DocumentNode 30 34 ): string => { 31 - let str = (typeof node !== 'string' 32 - ? (node.loc && node.loc.source.body) || print(node) 33 - : node 34 - ) 35 - .split(GRAPHQL_STRING_RE) 36 - .map(replaceOutsideStrings) 37 - .join(''); 38 - 39 - if (typeof node !== 'string') { 40 - const operationName = 'definitions' in node && getOperationName(node); 41 - if (operationName) { 42 - str = `# ${operationName}\n${str}`; 43 - } 35 + const printed = sanitizeDocument( 36 + typeof node === 'string' 37 + ? node 38 + : node.loc && node.loc.source.name === SOURCE_NAME 39 + ? node.loc.source.body 40 + : print(node) 41 + ); 44 42 45 - if (!node.loc) { 46 - (node as WritableLocation).loc = { 47 - start: 0, 48 - end: str.length, 49 - source: { 50 - body: str, 51 - name: 'gql', 52 - locationOffset: { line: 1, column: 1 }, 53 - }, 54 - } as Location; 55 - } 43 + if (typeof node !== 'string' && !node.loc) { 44 + (node as WritableLocation).loc = { 45 + start: 0, 46 + end: printed.length, 47 + source: { 48 + body: printed, 49 + name: SOURCE_NAME, 50 + locationOffset: { line: 1, column: 1 }, 51 + }, 52 + } as Location; 56 53 } 57 54 58 - return str; 55 + return printed; 59 56 }; 60 57 61 - const docs = new Map<number, KeyedDocumentNode>(); 58 + const hashDocument = ( 59 + node: string | DefinitionNode | DocumentNode 60 + ): HashValue => { 61 + let key = phash(stringifyDocument(node)); 62 + // Add the operation name to the produced hash 63 + if (typeof node === 'object' && 'definitions' in node) { 64 + const operationName = getOperationName(node); 65 + if (operationName) key = phash(`\n# ${operationName}`, key); 66 + } 67 + return key; 68 + }; 62 69 63 - export const keyDocument = (q: string | DocumentNode): KeyedDocumentNode => { 64 - let key: number; 70 + const docs = new Map<HashValue, KeyedDocumentNode>(); 71 + 72 + export const keyDocument = (node: string | DocumentNode): KeyedDocumentNode => { 73 + let key: HashValue; 65 74 let query: DocumentNode; 66 - if (typeof q === 'string') { 67 - key = hash(stringifyDocument(q)); 68 - query = docs.get(key) || parse(q, { noLocation: true }); 75 + if (typeof node === 'string') { 76 + key = hashDocument(node); 77 + query = docs.get(key) || parse(node, { noLocation: true }); 69 78 } else { 70 - key = (q as KeyedDocumentNode).__key || hash(stringifyDocument(q)); 71 - query = docs.get(key) || q; 79 + key = (node as KeyedDocumentNode).__key || hashDocument(node); 80 + query = docs.get(key) || node; 72 81 } 73 82 74 83 // Add location information if it's missing ··· 84 93 Variables extends AnyVariables = AnyVariables 85 94 >( 86 95 q: string | DocumentNode | TypedDocumentNode<Data, Variables>, 87 - vars: Variables 96 + variables: Variables 88 97 ): GraphQLRequest<Data, Variables> => { 89 - if (!vars) vars = {} as Variables; 98 + if (!variables) variables = {} as Variables; 90 99 const query = keyDocument(q); 91 - return { 92 - key: phash(query.__key, stringifyVariables(vars)) >>> 0, 93 - query, 94 - variables: vars as Variables, 95 - }; 100 + const printedVars = stringifyVariables(variables); 101 + let key = query.__key; 102 + if (printedVars !== '{}') key = phash(printedVars, key); 103 + return { key, query, variables }; 96 104 }; 97 105 98 106 /**
+8 -1
packages/svelte-urql/src/mutationStore.test.ts
··· 26 26 it('fills the store with correct values', () => { 27 27 expect(get(store).operation.kind).toBe('mutation'); 28 28 expect(get(store).operation.context.url).toBe('https://example.com'); 29 - expect(get(store).operation.query.loc?.source.body).toBe(query); 30 29 expect(get(store).operation.variables).toBe(variables); 30 + 31 + expect(get(store).operation.query.loc?.source.body).toMatchInlineSnapshot(` 32 + "mutation ($input: Example!) { 33 + doExample(input: $input) { 34 + id 35 + } 36 + }" 37 + `); 31 38 }); 32 39 });
+6 -1
packages/svelte-urql/src/queryStore.test.ts
··· 20 20 it('fills the store with correct values', () => { 21 21 expect(get(store).operation.kind).toBe('query'); 22 22 expect(get(store).operation.context.url).toBe('https://example.com'); 23 - expect(get(store).operation.query.loc?.source.body).toBe(query); 24 23 expect(get(store).operation.variables).toBe(variables); 24 + 25 + expect(get(store).operation.query.loc?.source.body).toMatchInlineSnapshot(` 26 + "{ 27 + test 28 + }" 29 + `); 25 30 }); 26 31 27 32 it('adds pause handles', () => {
+8 -1
packages/svelte-urql/src/subscriptionStore.test.ts
··· 25 25 it('fills the store with correct values', () => { 26 26 expect(get(store).operation.kind).toBe('subscription'); 27 27 expect(get(store).operation.context.url).toBe('https://example.com'); 28 - expect(get(store).operation.query.loc?.source.body).toBe(query); 29 28 expect(get(store).operation.variables).toBe(variables); 29 + 30 + expect(get(store).operation.query.loc?.source.body).toMatchInlineSnapshot(` 31 + "subscription ($input: ExampleInput) { 32 + exampleSubscribe(input: $input) { 33 + data 34 + } 35 + }" 36 + `); 30 37 }); 31 38 32 39 it('adds pause handles', () => {