From 310b07f5ca9242efa5f18f2973c1ab899fe296e1 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 18 Sep 2023 12:16:51 -0400 Subject: [PATCH 1/8] WIP throttle approach --- packages/sitecore-jss/package.json | 1 + .../sitecore-jss/src/layout/graphql-layout-service.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/packages/sitecore-jss/package.json b/packages/sitecore-jss/package.json index 2a0505ec8a..4ec000cd42 100644 --- a/packages/sitecore-jss/package.json +++ b/packages/sitecore-jss/package.json @@ -60,6 +60,7 @@ "graphql-request": "^4.2.0", "lodash.unescape": "^4.0.1", "memory-cache": "^0.2.0", + "p-throttle": "4.1.1", "url-parse": "^1.5.9" }, "description": "", diff --git a/packages/sitecore-jss/src/layout/graphql-layout-service.ts b/packages/sitecore-jss/src/layout/graphql-layout-service.ts index 187986d101..5e23424b83 100644 --- a/packages/sitecore-jss/src/layout/graphql-layout-service.ts +++ b/packages/sitecore-jss/src/layout/graphql-layout-service.ts @@ -2,6 +2,7 @@ import { LayoutServiceBase } from './layout-service'; import { LayoutServiceData } from './models'; import { GraphQLClient, GraphQLRequestClient } from '../graphql-request-client'; import debug from '../debug'; +import pThrottle from 'p-throttle'; export type GraphQLLayoutServiceConfig = { /** @@ -47,6 +48,8 @@ export class GraphQLLayoutService extends LayoutServiceBase { this.graphQLClient = this.getGraphQLClient(); } + private throttle = pThrottle({limit: 5, interval: 1000}) + /** * Fetch layout data for an item. * @param {string} itemPath item path to fetch layout data for. @@ -74,6 +77,13 @@ export class GraphQLLayoutService extends LayoutServiceBase { ); } + throttledFetch = this.throttle(async (itemPath: string, language?: string) => { + + const data = await this.fetchLayoutData(itemPath, language); + + return data +}) + /** * Gets a GraphQL client that can make requests to the API. Uses graphql-request as the default * library for fetching graphql data (@see GraphQLRequestClient). Override this method if you From 774db8f6969e906e906f10db8c138a0d0c4ccc87 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Sun, 24 Sep 2023 16:24:51 -0400 Subject: [PATCH 2/8] restore layout service and package to original state --- packages/sitecore-jss/package.json | 1 - .../src/layout/graphql-layout-service.ts | 12 +- yarn.lock | 235 ++++++++---------- 3 files changed, 110 insertions(+), 138 deletions(-) diff --git a/packages/sitecore-jss/package.json b/packages/sitecore-jss/package.json index 83ba909503..ff44e62fe1 100644 --- a/packages/sitecore-jss/package.json +++ b/packages/sitecore-jss/package.json @@ -60,7 +60,6 @@ "graphql-request": "^4.2.0", "lodash.unescape": "^4.0.1", "memory-cache": "^0.2.0", - "p-throttle": "4.1.1", "url-parse": "^1.5.9" }, "description": "", diff --git a/packages/sitecore-jss/src/layout/graphql-layout-service.ts b/packages/sitecore-jss/src/layout/graphql-layout-service.ts index 5e23424b83..a30b9ac1a5 100644 --- a/packages/sitecore-jss/src/layout/graphql-layout-service.ts +++ b/packages/sitecore-jss/src/layout/graphql-layout-service.ts @@ -2,7 +2,6 @@ import { LayoutServiceBase } from './layout-service'; import { LayoutServiceData } from './models'; import { GraphQLClient, GraphQLRequestClient } from '../graphql-request-client'; import debug from '../debug'; -import pThrottle from 'p-throttle'; export type GraphQLLayoutServiceConfig = { /** @@ -48,8 +47,6 @@ export class GraphQLLayoutService extends LayoutServiceBase { this.graphQLClient = this.getGraphQLClient(); } - private throttle = pThrottle({limit: 5, interval: 1000}) - /** * Fetch layout data for an item. * @param {string} itemPath item path to fetch layout data for. @@ -77,13 +74,6 @@ export class GraphQLLayoutService extends LayoutServiceBase { ); } - throttledFetch = this.throttle(async (itemPath: string, language?: string) => { - - const data = await this.fetchLayoutData(itemPath, language); - - return data -}) - /** * Gets a GraphQL client that can make requests to the API. Uses graphql-request as the default * library for fetching graphql data (@see GraphQLRequestClient). Override this method if you @@ -118,4 +108,4 @@ export class GraphQLLayoutService extends LayoutServiceBase { } }`; } -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 63b4c487df..e1be39eb46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5432,100 +5432,72 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:13.1.6": - version: 13.1.6 - resolution: "@next/env@npm:13.1.6" - checksum: 0f911a18f0b3372007632fffa87f5d7f802c00d07b3bf757d2d09574735ae43f60000ecdf64b6f06e195971c508c2bcee82dd1e3aab27a08a4300eb0317652bb +"@next/env@npm:13.5.2": + version: 13.5.2 + resolution: "@next/env@npm:13.5.2" + checksum: f6ef14b7643049dafc2d53b5091e3f74eed0af14743cfd61f1db7782a99e69b5bef63f36ba700034b23656a264c7ec498aac8fa4f9377dad01e544ffa507388f languageName: node linkType: hard -"@next/swc-android-arm-eabi@npm:13.1.6": - version: 13.1.6 - resolution: "@next/swc-android-arm-eabi@npm:13.1.6" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - -"@next/swc-android-arm64@npm:13.1.6": - version: 13.1.6 - resolution: "@next/swc-android-arm64@npm:13.1.6" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@next/swc-darwin-arm64@npm:13.1.6": - version: 13.1.6 - resolution: "@next/swc-darwin-arm64@npm:13.1.6" +"@next/swc-darwin-arm64@npm:13.5.2": + version: 13.5.2 + resolution: "@next/swc-darwin-arm64@npm:13.5.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@next/swc-darwin-x64@npm:13.1.6": - version: 13.1.6 - resolution: "@next/swc-darwin-x64@npm:13.1.6" +"@next/swc-darwin-x64@npm:13.5.2": + version: 13.5.2 + resolution: "@next/swc-darwin-x64@npm:13.5.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-freebsd-x64@npm:13.1.6": - version: 13.1.6 - resolution: "@next/swc-freebsd-x64@npm:13.1.6" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@next/swc-linux-arm-gnueabihf@npm:13.1.6": - version: 13.1.6 - resolution: "@next/swc-linux-arm-gnueabihf@npm:13.1.6" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@next/swc-linux-arm64-gnu@npm:13.1.6": - version: 13.1.6 - resolution: "@next/swc-linux-arm64-gnu@npm:13.1.6" +"@next/swc-linux-arm64-gnu@npm:13.5.2": + version: 13.5.2 + resolution: "@next/swc-linux-arm64-gnu@npm:13.5.2" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:13.1.6": - version: 13.1.6 - resolution: "@next/swc-linux-arm64-musl@npm:13.1.6" +"@next/swc-linux-arm64-musl@npm:13.5.2": + version: 13.5.2 + resolution: "@next/swc-linux-arm64-musl@npm:13.5.2" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:13.1.6": - version: 13.1.6 - resolution: "@next/swc-linux-x64-gnu@npm:13.1.6" +"@next/swc-linux-x64-gnu@npm:13.5.2": + version: 13.5.2 + resolution: "@next/swc-linux-x64-gnu@npm:13.5.2" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:13.1.6": - version: 13.1.6 - resolution: "@next/swc-linux-x64-musl@npm:13.1.6" +"@next/swc-linux-x64-musl@npm:13.5.2": + version: 13.5.2 + resolution: "@next/swc-linux-x64-musl@npm:13.5.2" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:13.1.6": - version: 13.1.6 - resolution: "@next/swc-win32-arm64-msvc@npm:13.1.6" +"@next/swc-win32-arm64-msvc@npm:13.5.2": + version: 13.5.2 + resolution: "@next/swc-win32-arm64-msvc@npm:13.5.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-ia32-msvc@npm:13.1.6": - version: 13.1.6 - resolution: "@next/swc-win32-ia32-msvc@npm:13.1.6" +"@next/swc-win32-ia32-msvc@npm:13.5.2": + version: 13.5.2 + resolution: "@next/swc-win32-ia32-msvc@npm:13.5.2" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:13.1.6": - version: 13.1.6 - resolution: "@next/swc-win32-x64-msvc@npm:13.1.6" +"@next/swc-win32-x64-msvc@npm:13.5.2": + version: 13.5.2 + resolution: "@next/swc-win32-x64-msvc@npm:13.5.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -6446,12 +6418,12 @@ __metadata: languageName: node linkType: hard -"@sitecore-feaas/clientside@npm:^0.3.14": - version: 0.3.14 - resolution: "@sitecore-feaas/clientside@npm:0.3.14" +"@sitecore-feaas/clientside@npm:^0.3.17": + version: 0.3.17 + resolution: "@sitecore-feaas/clientside@npm:0.3.17" peerDependencies: react-dom: ">=16.8.0" - checksum: e793a475862dfb4d9aaeff1e74745578c8eb2fe168ea85ac133a1f7b20f63c225ad2e82b536377e1b63dfc8f03e6a5d83f24217e89b03d035d329a0356f492aa + checksum: 2a3d7eec204fdb71c2787feedc5966ff0fbbd0387f38bb1d5ae8ca75ea63d5588ae79e0f06ba393bc66932186d76bdcf8a970ec3f55ef73016b85bd9bf9f141a languageName: node linkType: hard @@ -6485,7 +6457,7 @@ __metadata: "@angular/platform-browser": ~15.2.9 "@angular/platform-browser-dynamic": ~15.2.9 "@angular/router": ~15.2.9 - "@sitecore-jss/sitecore-jss": 21.4.0-canary.4 + "@sitecore-jss/sitecore-jss": 21.5.0-canary.1 "@types/jasmine": ^3.4.1 "@types/node": ^14.14.35 codelyzer: ^6.0.1 @@ -6516,7 +6488,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-cli@workspace:packages/sitecore-jss-cli" dependencies: - "@sitecore-jss/sitecore-jss-dev-tools": 21.4.0-canary.4 + "@sitecore-jss/sitecore-jss-dev-tools": 21.5.0-canary.1 "@types/chai": ^4.2.4 "@types/cross-spawn": ^6.0.2 "@types/mocha": ^10.0.1 @@ -6548,11 +6520,11 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-jss/sitecore-jss-dev-tools@21.4.0-canary.4, @sitecore-jss/sitecore-jss-dev-tools@workspace:packages/sitecore-jss-dev-tools": +"@sitecore-jss/sitecore-jss-dev-tools@21.5.0-canary.1, @sitecore-jss/sitecore-jss-dev-tools@workspace:packages/sitecore-jss-dev-tools": version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-dev-tools@workspace:packages/sitecore-jss-dev-tools" dependencies: - "@sitecore-jss/sitecore-jss": 21.4.0-canary.4 + "@sitecore-jss/sitecore-jss": 21.5.0-canary.1 "@types/chai": ^4.3.4 "@types/chokidar": ^2.1.3 "@types/del": ^4.0.0 @@ -6604,11 +6576,11 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-jss/sitecore-jss-forms@21.4.0-canary.4, @sitecore-jss/sitecore-jss-forms@workspace:packages/sitecore-jss-forms": +"@sitecore-jss/sitecore-jss-forms@21.5.0-canary.1, @sitecore-jss/sitecore-jss-forms@workspace:packages/sitecore-jss-forms": version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-forms@workspace:packages/sitecore-jss-forms" dependencies: - "@sitecore-jss/sitecore-jss": 21.4.0-canary.4 + "@sitecore-jss/sitecore-jss": 21.5.0-canary.1 "@types/chai": ^4.3.4 "@types/chai-string": ^1.4.2 "@types/lodash.unescape": ^4.0.7 @@ -6631,9 +6603,9 @@ __metadata: version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-nextjs@workspace:packages/sitecore-jss-nextjs" dependencies: - "@sitecore-jss/sitecore-jss": 21.4.0-canary.4 - "@sitecore-jss/sitecore-jss-dev-tools": 21.4.0-canary.4 - "@sitecore-jss/sitecore-jss-react": 21.4.0-canary.4 + "@sitecore-jss/sitecore-jss": 21.5.0-canary.1 + "@sitecore-jss/sitecore-jss-dev-tools": 21.5.0-canary.1 + "@sitecore-jss/sitecore-jss-react": 21.5.0-canary.1 "@types/chai": ^4.3.4 "@types/chai-as-promised": ^7.1.5 "@types/chai-string": ^1.4.2 @@ -6659,7 +6631,7 @@ __metadata: eslint-plugin-react: ^7.32.1 jsdom: ^21.1.0 mocha: ^10.2.0 - next: ^13.1.6 + next: ^13.4.16 nock: ^13.3.0 node-html-parser: ^6.1.4 nyc: ^15.1.0 @@ -6673,7 +6645,7 @@ __metadata: ts-node: ^10.9.1 typescript: ~4.9.4 peerDependencies: - next: ^13.1.6 + next: ^13.4.16 react: ^18.2.0 react-dom: ^18.2.0 languageName: unknown @@ -6705,7 +6677,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-react-forms@workspace:packages/sitecore-jss-react-forms" dependencies: - "@sitecore-jss/sitecore-jss-forms": 21.4.0-canary.4 + "@sitecore-jss/sitecore-jss-forms": 21.5.0-canary.1 "@types/chai": ^4.3.4 "@types/enzyme": ^3.10.12 "@types/mocha": ^10.0.1 @@ -6745,7 +6717,7 @@ __metadata: "@babel/plugin-proposal-export-default-from": ^7.5.2 "@babel/preset-env": ^7.6.2 "@babel/preset-typescript": ^7.6.0 - "@sitecore-jss/sitecore-jss": 21.4.0-canary.4 + "@sitecore-jss/sitecore-jss": 21.5.0-canary.1 "@types/jest": ^24.0.18 "@types/prop-types": ^15.7.3 "@types/react": ^16.9.5 @@ -6775,12 +6747,12 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-jss/sitecore-jss-react@21.4.0-canary.4, @sitecore-jss/sitecore-jss-react@workspace:packages/sitecore-jss-react": +"@sitecore-jss/sitecore-jss-react@21.5.0-canary.1, @sitecore-jss/sitecore-jss-react@workspace:packages/sitecore-jss-react": version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-react@workspace:packages/sitecore-jss-react" dependencies: - "@sitecore-feaas/clientside": ^0.3.14 - "@sitecore-jss/sitecore-jss": 21.4.0-canary.4 + "@sitecore-feaas/clientside": ^0.3.17 + "@sitecore-jss/sitecore-jss": 21.5.0-canary.1 "@types/chai": ^4.3.4 "@types/chai-string": ^1.4.2 "@types/deep-equal": ^1.0.1 @@ -6854,7 +6826,7 @@ __metadata: resolution: "@sitecore-jss/sitecore-jss-vue@workspace:packages/sitecore-jss-vue" dependencies: "@babel/core": ^7.20.12 - "@sitecore-jss/sitecore-jss": 21.4.0-canary.4 + "@sitecore-jss/sitecore-jss": 21.5.0-canary.1 "@types/jest": ^29.2.6 "@types/node": ^18.11.18 "@vue/compiler-dom": ^3.2.45 @@ -6878,7 +6850,7 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-jss/sitecore-jss@21.4.0-canary.4, @sitecore-jss/sitecore-jss@workspace:packages/sitecore-jss": +"@sitecore-jss/sitecore-jss@21.5.0-canary.1, @sitecore-jss/sitecore-jss@workspace:packages/sitecore-jss": version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss@workspace:packages/sitecore-jss" dependencies: @@ -6920,12 +6892,12 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:0.4.14": - version: 0.4.14 - resolution: "@swc/helpers@npm:0.4.14" +"@swc/helpers@npm:0.5.2": + version: 0.5.2 + resolution: "@swc/helpers@npm:0.5.2" dependencies: tslib: ^2.4.0 - checksum: 273fd3f3fc461a92f3790cc551ea054745c6d6959afbe1232e6d7aa1c722bbc114d308aab96bef5c78fc0303c85c7b472ef00e2253251cc89737f3b1af56e5a5 + checksum: 51d7e3d8bd56818c49d6bfbd715f0dbeedc13cf723af41166e45c03e37f109336bbcb57a1f2020f4015957721aeb21e1a7fff281233d797ff7d3dd1f447fa258 languageName: node linkType: hard @@ -11121,6 +11093,15 @@ __metadata: languageName: node linkType: hard +"busboy@npm:1.6.0": + version: 1.6.0 + resolution: "busboy@npm:1.6.0" + dependencies: + streamsearch: ^1.1.0 + checksum: 32801e2c0164e12106bf236291a00795c3c4e4b709ae02132883fe8478ba2ae23743b11c5735a0aae8afe65ac4b6ca4568b91f0d9fed1fdbc32ede824a73746e + languageName: node + linkType: hard + "byte-size@npm:^7.0.0": version: 7.0.1 resolution: "byte-size@npm:7.0.1" @@ -21479,47 +21460,37 @@ __metadata: languageName: node linkType: hard -"next@npm:^13.1.6": - version: 13.1.6 - resolution: "next@npm:13.1.6" - dependencies: - "@next/env": 13.1.6 - "@next/swc-android-arm-eabi": 13.1.6 - "@next/swc-android-arm64": 13.1.6 - "@next/swc-darwin-arm64": 13.1.6 - "@next/swc-darwin-x64": 13.1.6 - "@next/swc-freebsd-x64": 13.1.6 - "@next/swc-linux-arm-gnueabihf": 13.1.6 - "@next/swc-linux-arm64-gnu": 13.1.6 - "@next/swc-linux-arm64-musl": 13.1.6 - "@next/swc-linux-x64-gnu": 13.1.6 - "@next/swc-linux-x64-musl": 13.1.6 - "@next/swc-win32-arm64-msvc": 13.1.6 - "@next/swc-win32-ia32-msvc": 13.1.6 - "@next/swc-win32-x64-msvc": 13.1.6 - "@swc/helpers": 0.4.14 +"next@npm:^13.4.16": + version: 13.5.2 + resolution: "next@npm:13.5.2" + dependencies: + "@next/env": 13.5.2 + "@next/swc-darwin-arm64": 13.5.2 + "@next/swc-darwin-x64": 13.5.2 + "@next/swc-linux-arm64-gnu": 13.5.2 + "@next/swc-linux-arm64-musl": 13.5.2 + "@next/swc-linux-x64-gnu": 13.5.2 + "@next/swc-linux-x64-musl": 13.5.2 + "@next/swc-win32-arm64-msvc": 13.5.2 + "@next/swc-win32-ia32-msvc": 13.5.2 + "@next/swc-win32-x64-msvc": 13.5.2 + "@swc/helpers": 0.5.2 + busboy: 1.6.0 caniuse-lite: ^1.0.30001406 postcss: 8.4.14 styled-jsx: 5.1.1 + watchpack: 2.4.0 + zod: 3.21.4 peerDependencies: - fibers: ">= 3.1.0" - node-sass: ^6.0.0 || ^7.0.0 + "@opentelemetry/api": ^1.1.0 react: ^18.2.0 react-dom: ^18.2.0 sass: ^1.3.0 dependenciesMeta: - "@next/swc-android-arm-eabi": - optional: true - "@next/swc-android-arm64": - optional: true "@next/swc-darwin-arm64": optional: true "@next/swc-darwin-x64": optional: true - "@next/swc-freebsd-x64": - optional: true - "@next/swc-linux-arm-gnueabihf": - optional: true "@next/swc-linux-arm64-gnu": optional: true "@next/swc-linux-arm64-musl": @@ -21535,15 +21506,13 @@ __metadata: "@next/swc-win32-x64-msvc": optional: true peerDependenciesMeta: - fibers: - optional: true - node-sass: + "@opentelemetry/api": optional: true sass: optional: true bin: next: dist/bin/next - checksum: 584977e382bd826c21e7fc5f67bca50e4d95741a854b1686394d45331404479c7266569671227421975fc18e5cf70769a4ad7edede7450d4497213205bba77c8 + checksum: cc0635ad5aaab9fc1f4315b9506361b1abf1a12146542d6054b9434e2e892e967f19fbabd3f3763ba5e227306aa91627c1d73af089e9b853b84c74e20bb0be00 languageName: node linkType: hard @@ -26388,6 +26357,13 @@ __metadata: languageName: node linkType: hard +"streamsearch@npm:^1.1.0": + version: 1.1.0 + resolution: "streamsearch@npm:1.1.0" + checksum: 1cce16cea8405d7a233d32ca5e00a00169cc0e19fbc02aa839959985f267335d435c07f96e5e0edd0eadc6d39c98d5435fb5bbbdefc62c41834eadc5622ad942 + languageName: node + linkType: hard + "string-length@npm:^2.0.0": version: 2.0.0 resolution: "string-length@npm:2.0.0" @@ -28171,23 +28147,23 @@ __metadata: languageName: node linkType: hard -"watchpack@npm:^2.2.0": - version: 2.2.0 - resolution: "watchpack@npm:2.2.0" +"watchpack@npm:2.4.0, watchpack@npm:^2.4.0": + version: 2.4.0 + resolution: "watchpack@npm:2.4.0" dependencies: glob-to-regexp: ^0.4.1 graceful-fs: ^4.1.2 - checksum: e275f48fae29edee3195c51a8312b609581b9be5ce323d3102ffd082cb124f48d7a393ce05e4110239e4354379e04d78a97ceb26ae367746e7e218bf258135c8 + checksum: 23d4bc58634dbe13b86093e01c6a68d8096028b664ab7139d58f0c37d962d549a940e98f2f201cecdabd6f9c340338dc73ef8bf094a2249ef582f35183d1a131 languageName: node linkType: hard -"watchpack@npm:^2.4.0": - version: 2.4.0 - resolution: "watchpack@npm:2.4.0" +"watchpack@npm:^2.2.0": + version: 2.2.0 + resolution: "watchpack@npm:2.2.0" dependencies: glob-to-regexp: ^0.4.1 graceful-fs: ^4.1.2 - checksum: 23d4bc58634dbe13b86093e01c6a68d8096028b664ab7139d58f0c37d962d549a940e98f2f201cecdabd6f9c340338dc73ef8bf094a2249ef582f35183d1a131 + checksum: e275f48fae29edee3195c51a8312b609581b9be5ce323d3102ffd082cb124f48d7a393ce05e4110239e4354379e04d78a97ceb26ae367746e7e218bf258135c8 languageName: node linkType: hard @@ -29169,6 +29145,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:3.21.4": + version: 3.21.4 + resolution: "zod@npm:3.21.4" + checksum: f185ba87342ff16f7a06686767c2b2a7af41110c7edf7c1974095d8db7a73792696bcb4a00853de0d2edeb34a5b2ea6a55871bc864227dace682a0a28de33e1f + languageName: node + linkType: hard + "zone.js@npm:~0.10.3": version: 0.10.3 resolution: "zone.js@npm:0.10.3" From dc7f370c459cc29f421fb1db36f8e4de796b2b1f Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Sun, 24 Sep 2023 17:28:31 -0400 Subject: [PATCH 3/8] Retryer in graphql client implementation --- .../src/lib/dictionary-service-factory.ts | 6 ++ .../nextjs/src/lib/layout-service-factory.ts | 6 ++ .../src/graphql-request-client.test.ts | 81 +++++++++++++++++++ .../src/graphql-request-client.ts | 78 +++++++++++++----- .../src/i18n/graphql-dictionary-service.ts | 9 ++- .../src/layout/graphql-layout-service.ts | 11 +-- 6 files changed, 164 insertions(+), 27 deletions(-) diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts index 8ccfffde9c..97a879b5b7 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts @@ -27,6 +27,12 @@ export class DictionaryServiceFactory { Otherwise, if your Sitecore instance only has 1 JSS App (i.e. in a Sitecore XP setup), you can specify the root item ID here. rootItemId: '{GUID}' */ + /* + GraphQL Dictionary and Layout Services can handle 429 code errors from server. + For this, specify the number of retries the GraphQL server will attempt. + It will only try the request once by default + retries: %number% + */ }) : new RestDictionaryService({ apiHost: config.sitecoreApiHost, diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts index c471eacb3b..64ff5d915c 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts @@ -20,6 +20,12 @@ export class LayoutServiceFactory { endpoint: config.graphQLEndpoint, apiKey: config.sitecoreApiKey, siteName, + /* + GraphQL Dictionary and Layout Services can handle 429 code errors from server. + For this, specify the number of retries the GraphQL server will attempt. + It will only try the request once by default + retries: %number% + */ }) : new RestLayoutService({ apiHost: config.sitecoreApiHost, diff --git a/packages/sitecore-jss/src/graphql-request-client.test.ts b/packages/sitecore-jss/src/graphql-request-client.test.ts index 51bbd2e93b..55bb35e2cc 100644 --- a/packages/sitecore-jss/src/graphql-request-client.test.ts +++ b/packages/sitecore-jss/src/graphql-request-client.test.ts @@ -1,4 +1,5 @@ /* eslint-disable no-unused-expressions */ +/* eslint-disable dot-notation */ import { expect, use, spy } from 'chai'; import spies from 'chai-spies'; import nock from 'nock'; @@ -134,6 +135,86 @@ describe('GraphQLRequestClient', () => { }); }); + it('should use retry and throw error when retries specified', async function() { + this.timeout(6000); + nock('http://jssnextweb') + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(429); + const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 3 }); + spy.on(graphQLClient['client'], 'request'); + await graphQLClient.request('test').catch((error) => { + expect(error).to.not.be.undefined; + expect(graphQLClient['client'].request).to.be.called.exactly(3); + spy.restore(graphQLClient); + }); + }); + + it('should use retry and resolve if one of the requests resolves', async function() { + this.timeout(6000); + nock('http://jssnextweb') + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(200, { + data: { + result: 'Hello world...', + }, + }); + const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 3 }); + spy.on(graphQLClient['client'], 'request'); + + const data = await graphQLClient.request('test'); + + expect(data).to.not.be.null; + expect(graphQLClient['client'].request).to.be.called.exactly(3); + spy.restore(graphQLClient); + }); + + it('should use [retry-after] header value when response is 429', async function() { + this.timeout(6000); + nock('http://jssnextweb') + .post('/graphql') + .reply(429, {}, { 'Retry-After': '2' }); + const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 2 }); + spy.on(graphQLClient, 'debug'); + + await graphQLClient.request('test').catch(() => { + expect(graphQLClient['debug']).to.have.been.called.with( + 'Endpoint responded with 429. Retrying in %ds. Retries left: %d', + 1, + 2 + ); + spy.restore(graphQLClient); + }); + }); + + it('should throw error when request is aborted with default timeout value after retry', async () => { + nock('http://jssnextweb') + .post('/graphql') + .reply(429) + .post('/graphql') + .delay(100) + .reply(200, { + data: { + result: 'Hello world...', + }, + }); + + const graphQLClient = new GraphQLRequestClient(endpoint, {retries: 2}); + spy.on(graphQLClient['client'], 'request'); + await graphQLClient.request('test').catch((error) => { + expect(graphQLClient['client'].request).to.be.called.exactly(2); + expect(error.name).to.equal('AbortError'); + spy.restore(graphQLClient); + }); + }); + it('should throw error upon request timeout using provided timeout value', async () => { nock('http://jssnextweb') .post('/graphql') diff --git a/packages/sitecore-jss/src/graphql-request-client.ts b/packages/sitecore-jss/src/graphql-request-client.ts index 6300a176a1..ec23a6132f 100644 --- a/packages/sitecore-jss/src/graphql-request-client.ts +++ b/packages/sitecore-jss/src/graphql-request-client.ts @@ -13,7 +13,20 @@ export interface GraphQLClient { * @param {string | DocumentNode} query graphql query * @param {Object} variables graphql variables */ - request(query: string | DocumentNode, variables?: { [key: string]: unknown }): Promise; + request( + query: string | DocumentNode, + variables?: { [key: string]: unknown }, + ): Promise; +} + +/** + * Interface for graphql services that utilize retry functionality from GraphQL client + */ +export interface GraphQLServiceRetryConfig { + /** + * Number of retries to pass into graphql client configuration + */ + retries?: number } /** @@ -36,6 +49,10 @@ export type GraphQLRequestClientConfig = { * GraphQLClient request timeout */ timeout?: number; + /** + * Number of retries for client + */ + retries?: number; }; /** @@ -48,6 +65,7 @@ export class GraphQLRequestClient implements GraphQLClient { private debug: Debugger; private abortTimeout?: TimeoutPromise; private timeout?: number; + private retries?: number; /** * Provides ability to execute graphql query using given `endpoint` @@ -66,6 +84,7 @@ export class GraphQLRequestClient implements GraphQLClient { } this.timeout = clientConfig.timeout; + this.retries = clientConfig.retries; this.client = new Client(endpoint, { headers: this.headers, fetch: clientConfig.fetch, @@ -82,8 +101,6 @@ export class GraphQLRequestClient implements GraphQLClient { query: string | DocumentNode, variables?: { [key: string]: unknown } ): Promise { - const startTimestamp = Date.now(); - return new Promise((resolve, reject) => { // Note we don't have access to raw request/response with graphql-request // (or nice hooks like we have with Axios), but we should log whatever we have. @@ -93,23 +110,48 @@ export class GraphQLRequestClient implements GraphQLClient { query, variables, }); + let retriesLeft = this.retries || 1; - const fetchWithOptionalTimeout = [this.client.request(query, variables)]; - if (this.timeout) { - this.abortTimeout = new TimeoutPromise(this.timeout); - fetchWithOptionalTimeout.push(this.abortTimeout.start); - } - Promise.race(fetchWithOptionalTimeout).then( - (data: T) => { - this.abortTimeout?.clear(); - this.debug('response in %dms: %o', Date.now() - startTimestamp, data); - resolve(data); - }, - (error: ClientError) => { - this.abortTimeout?.clear(); - this.debug('response error: %o', error.response || error.message || error); - reject(error); + const retryer = async (): Promise => { + const startTimestamp = Date.now(); + retriesLeft--; + const fetchWithOptionalTimeout = [this.client.request(query, variables)]; + if (this.timeout) { + this.abortTimeout = new TimeoutPromise(this.timeout); + fetchWithOptionalTimeout.push(this.abortTimeout.start); } + return Promise.race(fetchWithOptionalTimeout).then( + (data: T) => { + this.abortTimeout?.clear(); + this.debug('response in %dms: %o', Date.now() - startTimestamp, data); + return Promise.resolve(data); + }, + (error: ClientError) => { + this.abortTimeout?.clear(); + this.debug('response error: %o', error.response || error.message || error); + if (error.response?.status === 429 && retriesLeft > 0) { + const rawHeaders = (error as ClientError)?.response?.headers; + const delaySeconds = + rawHeaders && rawHeaders.get('Retry-After') + ? Number.parseInt(rawHeaders.get('Retry-After'), 10) + : 1; + this.debug( + 'Endpoint responded with 429. Retrying in %ds. Retries left: %d', + delaySeconds, + retriesLeft + ); + return new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000)).then(() => { + return retryer(); + }); + } else { + return Promise.reject(error); + } + } + ); + }; + retryer().then( + (data) => resolve(data), + (error: ClientError) => reject(error) ); }); } diff --git a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts index 50be55aee1..6a1ee90f1b 100644 --- a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts +++ b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts @@ -1,4 +1,4 @@ -import { GraphQLClient, GraphQLRequestClient } from '../graphql-request-client'; +import { GraphQLClient, GraphQLRequestClient, GraphQLServiceRetryConfig } from '../graphql-request-client'; import { SitecoreTemplateId } from '../constants'; import { DictionaryPhrases, DictionaryServiceBase } from './dictionary-service'; import { CacheOptions } from '../cache-client'; @@ -48,7 +48,7 @@ const query = /* GraphQL */ ` /** * Configuration options for @see GraphQLDictionaryService instances */ -export interface GraphQLDictionaryServiceConfig extends SearchServiceConfig, CacheOptions { +export interface GraphQLDictionaryServiceConfig extends SearchServiceConfig, CacheOptions, GraphQLServiceRetryConfig { /** * The URL of the graphQL endpoint. */ @@ -95,7 +95,7 @@ export class GraphQLDictionaryService extends DictionaryServiceBase { */ constructor(public options: GraphQLDictionaryServiceConfig) { super(options); - this.graphQLClient = this.getGraphQLClient(); + this.graphQLClient = this.getGraphQLClient(options.retries); this.searchService = new SearchQueryService(this.graphQLClient); } @@ -153,10 +153,11 @@ export class GraphQLDictionaryService extends DictionaryServiceBase { * want to use something else. * @returns {GraphQLClient} implementation */ - protected getGraphQLClient(): GraphQLClient { + protected getGraphQLClient(retries?: number): GraphQLClient { return new GraphQLRequestClient(this.options.endpoint, { apiKey: this.options.apiKey, debugger: debug.dictionary, + retries, }); } } diff --git a/packages/sitecore-jss/src/layout/graphql-layout-service.ts b/packages/sitecore-jss/src/layout/graphql-layout-service.ts index a30b9ac1a5..5f345a5add 100644 --- a/packages/sitecore-jss/src/layout/graphql-layout-service.ts +++ b/packages/sitecore-jss/src/layout/graphql-layout-service.ts @@ -1,9 +1,9 @@ import { LayoutServiceBase } from './layout-service'; import { LayoutServiceData } from './models'; -import { GraphQLClient, GraphQLRequestClient } from '../graphql-request-client'; +import { GraphQLClient, GraphQLRequestClient, GraphQLServiceRetryConfig } from '../graphql-request-client'; import debug from '../debug'; -export type GraphQLLayoutServiceConfig = { +export interface GraphQLLayoutServiceConfig extends GraphQLServiceRetryConfig { /** * Your Graphql endpoint */ @@ -44,7 +44,7 @@ export class GraphQLLayoutService extends LayoutServiceBase { */ constructor(public serviceConfig: GraphQLLayoutServiceConfig) { super(); - this.graphQLClient = this.getGraphQLClient(); + this.graphQLClient = this.getGraphQLClient(serviceConfig.retries); } /** @@ -80,10 +80,11 @@ export class GraphQLLayoutService extends LayoutServiceBase { * want to use something else. * @returns {GraphQLClient} implementation */ - protected getGraphQLClient(): GraphQLClient { + protected getGraphQLClient(retries?: number): GraphQLClient { return new GraphQLRequestClient(this.serviceConfig.endpoint, { apiKey: this.serviceConfig.apiKey, debugger: debug.layout, + retries }); } @@ -108,4 +109,4 @@ export class GraphQLLayoutService extends LayoutServiceBase { } }`; } -} \ No newline at end of file +} From 89cfd04139dbebc3a6c57255f1edbb7890590e35 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Sun, 24 Sep 2023 17:55:16 -0400 Subject: [PATCH 4/8] one extra lint in sitecore-jss package --- .../sitecore-jss/src/graphql-request-client.test.ts | 2 +- packages/sitecore-jss/src/graphql-request-client.ts | 7 ++----- .../src/i18n/graphql-dictionary-service.ts | 12 ++++++++++-- .../src/layout/graphql-layout-service.ts | 11 ++++++++--- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/sitecore-jss/src/graphql-request-client.test.ts b/packages/sitecore-jss/src/graphql-request-client.test.ts index 55bb35e2cc..c637b93329 100644 --- a/packages/sitecore-jss/src/graphql-request-client.test.ts +++ b/packages/sitecore-jss/src/graphql-request-client.test.ts @@ -206,7 +206,7 @@ describe('GraphQLRequestClient', () => { }, }); - const graphQLClient = new GraphQLRequestClient(endpoint, {retries: 2}); + const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 2 }); spy.on(graphQLClient['client'], 'request'); await graphQLClient.request('test').catch((error) => { expect(graphQLClient['client'].request).to.be.called.exactly(2); diff --git a/packages/sitecore-jss/src/graphql-request-client.ts b/packages/sitecore-jss/src/graphql-request-client.ts index ec23a6132f..97a4f1e317 100644 --- a/packages/sitecore-jss/src/graphql-request-client.ts +++ b/packages/sitecore-jss/src/graphql-request-client.ts @@ -13,10 +13,7 @@ export interface GraphQLClient { * @param {string | DocumentNode} query graphql query * @param {Object} variables graphql variables */ - request( - query: string | DocumentNode, - variables?: { [key: string]: unknown }, - ): Promise; + request(query: string | DocumentNode, variables?: { [key: string]: unknown }): Promise; } /** @@ -26,7 +23,7 @@ export interface GraphQLServiceRetryConfig { /** * Number of retries to pass into graphql client configuration */ - retries?: number + retries?: number; } /** diff --git a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts index 6a1ee90f1b..b9cc22a8ef 100644 --- a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts +++ b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts @@ -1,4 +1,8 @@ -import { GraphQLClient, GraphQLRequestClient, GraphQLServiceRetryConfig } from '../graphql-request-client'; +import { + GraphQLClient, + GraphQLRequestClient, + GraphQLServiceRetryConfig, +} from '../graphql-request-client'; import { SitecoreTemplateId } from '../constants'; import { DictionaryPhrases, DictionaryServiceBase } from './dictionary-service'; import { CacheOptions } from '../cache-client'; @@ -48,7 +52,10 @@ const query = /* GraphQL */ ` /** * Configuration options for @see GraphQLDictionaryService instances */ -export interface GraphQLDictionaryServiceConfig extends SearchServiceConfig, CacheOptions, GraphQLServiceRetryConfig { +export interface GraphQLDictionaryServiceConfig + extends SearchServiceConfig, + CacheOptions, + GraphQLServiceRetryConfig { /** * The URL of the graphQL endpoint. */ @@ -151,6 +158,7 @@ export class GraphQLDictionaryService extends DictionaryServiceBase { * Gets a GraphQL client that can make requests to the API. Uses graphql-request as the default * library for fetching graphql data (@see GraphQLRequestClient). Override this method if you * want to use something else. + * @param {number} retries number of retries a graphql client should attempt * @returns {GraphQLClient} implementation */ protected getGraphQLClient(retries?: number): GraphQLClient { diff --git a/packages/sitecore-jss/src/layout/graphql-layout-service.ts b/packages/sitecore-jss/src/layout/graphql-layout-service.ts index 5f345a5add..11d349822b 100644 --- a/packages/sitecore-jss/src/layout/graphql-layout-service.ts +++ b/packages/sitecore-jss/src/layout/graphql-layout-service.ts @@ -1,6 +1,10 @@ import { LayoutServiceBase } from './layout-service'; import { LayoutServiceData } from './models'; -import { GraphQLClient, GraphQLRequestClient, GraphQLServiceRetryConfig } from '../graphql-request-client'; +import { + GraphQLClient, + GraphQLRequestClient, + GraphQLServiceRetryConfig, +} from '../graphql-request-client'; import debug from '../debug'; export interface GraphQLLayoutServiceConfig extends GraphQLServiceRetryConfig { @@ -28,7 +32,7 @@ export interface GraphQLLayoutServiceConfig extends GraphQLServiceRetryConfig { * layout(site:"${siteName}", routePath:"${itemPath}", language:"${language}") */ formatLayoutQuery?: (siteName: string, itemPath: string, locale?: string) => string; -}; +} /** * Service that fetch layout data using Sitecore's GraphQL API. @@ -78,13 +82,14 @@ export class GraphQLLayoutService extends LayoutServiceBase { * Gets a GraphQL client that can make requests to the API. Uses graphql-request as the default * library for fetching graphql data (@see GraphQLRequestClient). Override this method if you * want to use something else. + * @param {number} retries number of retries a graphql client should attempt * @returns {GraphQLClient} implementation */ protected getGraphQLClient(retries?: number): GraphQLClient { return new GraphQLRequestClient(this.serviceConfig.endpoint, { apiKey: this.serviceConfig.apiKey, debugger: debug.layout, - retries + retries, }); } From a7b796a6196cbd58d60ea9a7a4b64a47f0e809a6 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 25 Sep 2023 09:11:46 -0400 Subject: [PATCH 5/8] address PR comments: fix comments, adjust constructors --- .../src/lib/dictionary-service-factory.ts | 9 +++++---- .../nextjs/src/lib/layout-service-factory.ts | 9 +++++---- .../src/graphql-request-client.test.ts | 2 +- .../src/graphql-request-client.ts | 20 +++++-------------- .../src/i18n/graphql-dictionary-service.ts | 11 +++++----- .../src/layout/graphql-layout-service.ts | 11 +++++----- 6 files changed, 26 insertions(+), 36 deletions(-) diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts index 97a879b5b7..dc1c81e32c 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts @@ -28,10 +28,11 @@ export class DictionaryServiceFactory { rootItemId: '{GUID}' */ /* - GraphQL Dictionary and Layout Services can handle 429 code errors from server. - For this, specify the number of retries the GraphQL server will attempt. - It will only try the request once by default - retries: %number% + GraphQL endpoint may reach its rate limit with the amount of Layout and Dictionary requests it receives and throw a rate limit error. + GraphQL Dictionary and Layout Services can handle rate limit errors from server and attempt a retry on requests. + For this, specify the number of retries the GraphQL client will attempt. + It will only try the request once by default. + retries: 'number' */ }) : new RestDictionaryService({ diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts index 64ff5d915c..07a7803981 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts @@ -21,10 +21,11 @@ export class LayoutServiceFactory { apiKey: config.sitecoreApiKey, siteName, /* - GraphQL Dictionary and Layout Services can handle 429 code errors from server. - For this, specify the number of retries the GraphQL server will attempt. - It will only try the request once by default - retries: %number% + GraphQL endpoint may reach its rate limit with the amount of Layout and Dictionary requests it receives and throw a rate limit error. + GraphQL Dictionary and Layout Services can handle rate limit errors from server and attempt a retry on requests. + For this, specify the number of retries the GraphQL client will attempt. + It will only try the request once by default. + retries: 'number' */ }) : new RestLayoutService({ diff --git a/packages/sitecore-jss/src/graphql-request-client.test.ts b/packages/sitecore-jss/src/graphql-request-client.test.ts index c637b93329..01faac57cd 100644 --- a/packages/sitecore-jss/src/graphql-request-client.test.ts +++ b/packages/sitecore-jss/src/graphql-request-client.test.ts @@ -186,7 +186,7 @@ describe('GraphQLRequestClient', () => { await graphQLClient.request('test').catch(() => { expect(graphQLClient['debug']).to.have.been.called.with( - 'Endpoint responded with 429. Retrying in %ds. Retries left: %d', + 'Error: Rate limit reached for GraphQL endpoint. Retrying in %ds. Retries left: %d', 1, 2 ); diff --git a/packages/sitecore-jss/src/graphql-request-client.ts b/packages/sitecore-jss/src/graphql-request-client.ts index 97a4f1e317..a5922a0e64 100644 --- a/packages/sitecore-jss/src/graphql-request-client.ts +++ b/packages/sitecore-jss/src/graphql-request-client.ts @@ -16,16 +16,6 @@ export interface GraphQLClient { request(query: string | DocumentNode, variables?: { [key: string]: unknown }): Promise; } -/** - * Interface for graphql services that utilize retry functionality from GraphQL client - */ -export interface GraphQLServiceRetryConfig { - /** - * Number of retries to pass into graphql client configuration - */ - retries?: number; -} - /** * Minimum configuration options for classes that implement @see GraphQLClient */ @@ -47,7 +37,7 @@ export type GraphQLRequestClientConfig = { */ timeout?: number; /** - * Number of retries for client + * Number of retries for client. Will be used if endpoint responds with 429 (rate limit reached) error */ retries?: number; }; @@ -60,9 +50,9 @@ export class GraphQLRequestClient implements GraphQLClient { private client: Client; private headers: Record = {}; private debug: Debugger; + private retries: number; private abortTimeout?: TimeoutPromise; private timeout?: number; - private retries?: number; /** * Provides ability to execute graphql query using given `endpoint` @@ -81,7 +71,7 @@ export class GraphQLRequestClient implements GraphQLClient { } this.timeout = clientConfig.timeout; - this.retries = clientConfig.retries; + this.retries = clientConfig.retries || 1; this.client = new Client(endpoint, { headers: this.headers, fetch: clientConfig.fetch, @@ -107,7 +97,7 @@ export class GraphQLRequestClient implements GraphQLClient { query, variables, }); - let retriesLeft = this.retries || 1; + let retriesLeft = this.retries; const retryer = async (): Promise => { const startTimestamp = Date.now(); @@ -133,7 +123,7 @@ export class GraphQLRequestClient implements GraphQLClient { ? Number.parseInt(rawHeaders.get('Retry-After'), 10) : 1; this.debug( - 'Endpoint responded with 429. Retrying in %ds. Retries left: %d', + 'Error: Rate limit reached for GraphQL endpoint. Retrying in %ds. Retries left: %d', delaySeconds, retriesLeft ); diff --git a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts index b9cc22a8ef..e378bdfcc5 100644 --- a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts +++ b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts @@ -1,7 +1,7 @@ import { GraphQLClient, GraphQLRequestClient, - GraphQLServiceRetryConfig, + GraphQLRequestClientConfig, } from '../graphql-request-client'; import { SitecoreTemplateId } from '../constants'; import { DictionaryPhrases, DictionaryServiceBase } from './dictionary-service'; @@ -55,7 +55,7 @@ const query = /* GraphQL */ ` export interface GraphQLDictionaryServiceConfig extends SearchServiceConfig, CacheOptions, - GraphQLServiceRetryConfig { + Pick { /** * The URL of the graphQL endpoint. */ @@ -102,7 +102,7 @@ export class GraphQLDictionaryService extends DictionaryServiceBase { */ constructor(public options: GraphQLDictionaryServiceConfig) { super(options); - this.graphQLClient = this.getGraphQLClient(options.retries); + this.graphQLClient = this.getGraphQLClient(); this.searchService = new SearchQueryService(this.graphQLClient); } @@ -158,14 +158,13 @@ export class GraphQLDictionaryService extends DictionaryServiceBase { * Gets a GraphQL client that can make requests to the API. Uses graphql-request as the default * library for fetching graphql data (@see GraphQLRequestClient). Override this method if you * want to use something else. - * @param {number} retries number of retries a graphql client should attempt * @returns {GraphQLClient} implementation */ - protected getGraphQLClient(retries?: number): GraphQLClient { + protected getGraphQLClient(): GraphQLClient { return new GraphQLRequestClient(this.options.endpoint, { apiKey: this.options.apiKey, debugger: debug.dictionary, - retries, + retries: this.options.retries, }); } } diff --git a/packages/sitecore-jss/src/layout/graphql-layout-service.ts b/packages/sitecore-jss/src/layout/graphql-layout-service.ts index 11d349822b..5f7c59b9a2 100644 --- a/packages/sitecore-jss/src/layout/graphql-layout-service.ts +++ b/packages/sitecore-jss/src/layout/graphql-layout-service.ts @@ -3,11 +3,11 @@ import { LayoutServiceData } from './models'; import { GraphQLClient, GraphQLRequestClient, - GraphQLServiceRetryConfig, + GraphQLRequestClientConfig, } from '../graphql-request-client'; import debug from '../debug'; -export interface GraphQLLayoutServiceConfig extends GraphQLServiceRetryConfig { +export interface GraphQLLayoutServiceConfig extends Pick { /** * Your Graphql endpoint */ @@ -48,7 +48,7 @@ export class GraphQLLayoutService extends LayoutServiceBase { */ constructor(public serviceConfig: GraphQLLayoutServiceConfig) { super(); - this.graphQLClient = this.getGraphQLClient(serviceConfig.retries); + this.graphQLClient = this.getGraphQLClient(); } /** @@ -82,14 +82,13 @@ export class GraphQLLayoutService extends LayoutServiceBase { * Gets a GraphQL client that can make requests to the API. Uses graphql-request as the default * library for fetching graphql data (@see GraphQLRequestClient). Override this method if you * want to use something else. - * @param {number} retries number of retries a graphql client should attempt * @returns {GraphQLClient} implementation */ - protected getGraphQLClient(retries?: number): GraphQLClient { + protected getGraphQLClient(): GraphQLClient { return new GraphQLRequestClient(this.serviceConfig.endpoint, { apiKey: this.serviceConfig.apiKey, debugger: debug.layout, - retries, + retries: this.serviceConfig.retries, }); } From c9eb770cbf175d7c6714145a9dad389c2ca7976a Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 25 Sep 2023 09:16:24 -0400 Subject: [PATCH 6/8] address PR comments: simplify promise resolution in client --- packages/sitecore-jss/src/graphql-request-client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sitecore-jss/src/graphql-request-client.ts b/packages/sitecore-jss/src/graphql-request-client.ts index a5922a0e64..62365785bb 100644 --- a/packages/sitecore-jss/src/graphql-request-client.ts +++ b/packages/sitecore-jss/src/graphql-request-client.ts @@ -127,9 +127,9 @@ export class GraphQLRequestClient implements GraphQLClient { delaySeconds, retriesLeft ); - return new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000)).then(() => { - return retryer(); - }); + return new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000)).then( + retryer + ); } else { return Promise.reject(error); } From b879b95176b8236d871a3c505fc4359615b44ce4 Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 25 Sep 2023 11:34:08 -0400 Subject: [PATCH 7/8] changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65ddd16e27..c9a76b9908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Our versioning strategy is as follows: * `[sitecore-jss-dev-tools]` `[templates/nextjs]` `[templates/react]` Introduce "components" configuration for ComponentBuilder ([#1598](https://github.com/Sitecore/jss/pull/1598)) * `[sitecore-jss-react]` `[sitecore-jss-nextjs]` Component level data fetching(SSR/SSG) for BYOC ([#1610](https://github.com/Sitecore/jss/pull/1610)) * `[sitecore-jss-nextjs]` Reduce the amount of Edge API calls during fetch getStaticPaths ([#1612](https://github.com/Sitecore/jss/pull/1612)) +* `[sitecore-jss]` `[templates/nextjs]` GraphQL Layout and Dictionary services can handle endpoint rate limits through retryer functionality in GraphQLClient. To prevent SSG builds from failing and enable multiple retries, set retry amount in lib/dictionary-service-factory and lib/layout-service-factory ([#1618](https://github.com/Sitecore/jss/pull/1618)) ### 🧹 Chores From 408fad8cb259102ba7d4963618bda7b5a7ffc86d Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko Date: Mon, 25 Sep 2023 12:14:10 -0400 Subject: [PATCH 8/8] simplify promises implementation --- .../src/graphql-request-client.test.ts | 8 +- .../src/graphql-request-client.ts | 82 +++++++++---------- 2 files changed, 42 insertions(+), 48 deletions(-) diff --git a/packages/sitecore-jss/src/graphql-request-client.test.ts b/packages/sitecore-jss/src/graphql-request-client.test.ts index 01faac57cd..89d76fddb9 100644 --- a/packages/sitecore-jss/src/graphql-request-client.test.ts +++ b/packages/sitecore-jss/src/graphql-request-client.test.ts @@ -144,7 +144,7 @@ describe('GraphQLRequestClient', () => { .reply(429) .post('/graphql') .reply(429); - const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 3 }); + const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 2 }); spy.on(graphQLClient['client'], 'request'); await graphQLClient.request('test').catch((error) => { expect(error).to.not.be.undefined; @@ -181,14 +181,14 @@ describe('GraphQLRequestClient', () => { nock('http://jssnextweb') .post('/graphql') .reply(429, {}, { 'Retry-After': '2' }); - const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 2 }); + const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 1 }); spy.on(graphQLClient, 'debug'); await graphQLClient.request('test').catch(() => { expect(graphQLClient['debug']).to.have.been.called.with( 'Error: Rate limit reached for GraphQL endpoint. Retrying in %ds. Retries left: %d', - 1, - 2 + 2, + 1 ); spy.restore(graphQLClient); }); diff --git a/packages/sitecore-jss/src/graphql-request-client.ts b/packages/sitecore-jss/src/graphql-request-client.ts index 62365785bb..3e9dac24d7 100644 --- a/packages/sitecore-jss/src/graphql-request-client.ts +++ b/packages/sitecore-jss/src/graphql-request-client.ts @@ -71,7 +71,7 @@ export class GraphQLRequestClient implements GraphQLClient { } this.timeout = clientConfig.timeout; - this.retries = clientConfig.retries || 1; + this.retries = clientConfig.retries || 0; this.client = new Client(endpoint, { headers: this.headers, fetch: clientConfig.fetch, @@ -88,7 +88,9 @@ export class GraphQLRequestClient implements GraphQLClient { query: string | DocumentNode, variables?: { [key: string]: unknown } ): Promise { - return new Promise((resolve, reject) => { + let retriesLeft = this.retries; + + const retryer = async (): Promise => { // Note we don't have access to raw request/response with graphql-request // (or nice hooks like we have with Axios), but we should log whatever we have. this.debug('request: %o', { @@ -97,49 +99,41 @@ export class GraphQLRequestClient implements GraphQLClient { query, variables, }); - let retriesLeft = this.retries; - - const retryer = async (): Promise => { - const startTimestamp = Date.now(); - retriesLeft--; - const fetchWithOptionalTimeout = [this.client.request(query, variables)]; - if (this.timeout) { - this.abortTimeout = new TimeoutPromise(this.timeout); - fetchWithOptionalTimeout.push(this.abortTimeout.start); - } - return Promise.race(fetchWithOptionalTimeout).then( - (data: T) => { - this.abortTimeout?.clear(); - this.debug('response in %dms: %o', Date.now() - startTimestamp, data); - return Promise.resolve(data); - }, - (error: ClientError) => { - this.abortTimeout?.clear(); - this.debug('response error: %o', error.response || error.message || error); - if (error.response?.status === 429 && retriesLeft > 0) { - const rawHeaders = (error as ClientError)?.response?.headers; - const delaySeconds = - rawHeaders && rawHeaders.get('Retry-After') - ? Number.parseInt(rawHeaders.get('Retry-After'), 10) - : 1; - this.debug( - 'Error: Rate limit reached for GraphQL endpoint. Retrying in %ds. Retries left: %d', - delaySeconds, - retriesLeft - ); - return new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000)).then( - retryer - ); - } else { - return Promise.reject(error); - } + const startTimestamp = Date.now(); + const fetchWithOptionalTimeout = [this.client.request(query, variables)]; + if (this.timeout) { + this.abortTimeout = new TimeoutPromise(this.timeout); + fetchWithOptionalTimeout.push(this.abortTimeout.start); + } + return Promise.race(fetchWithOptionalTimeout).then( + (data: T) => { + this.abortTimeout?.clear(); + this.debug('response in %dms: %o', Date.now() - startTimestamp, data); + return Promise.resolve(data); + }, + (error: ClientError) => { + this.abortTimeout?.clear(); + this.debug('response error: %o', error.response || error.message || error); + if (error.response?.status === 429 && retriesLeft > 0) { + const rawHeaders = (error as ClientError)?.response?.headers; + const delaySeconds = + rawHeaders && rawHeaders.get('Retry-After') + ? Number.parseInt(rawHeaders.get('Retry-After'), 10) + : 1; + this.debug( + 'Error: Rate limit reached for GraphQL endpoint. Retrying in %ds. Retries left: %d', + delaySeconds, + retriesLeft + ); + retriesLeft--; + return new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000)).then(retryer); + } else { + return Promise.reject(error); } - ); - }; - retryer().then( - (data) => resolve(data), - (error: ClientError) => reject(error) + } ); - }); + }; + + return retryer(); } }