From 7df5b6f548caa7bc9e92492cfc5514a77c9f7f25 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 20 Oct 2023 10:19:14 +0200 Subject: [PATCH 01/51] checkpoint --- packages/providers/codegen.json | 2 +- packages/providers/package.json | 9 +- packages/providers/src/operations.graphql | 59 ++++++---- packages/providers/src/provider.ts | 21 +++- packages/providers/src/ssejs.d.ts | 14 +++ packages/providers/src/subscription-client.ts | 24 ++++ pnpm-lock.yaml | 109 ++++++++++++++---- 7 files changed, 188 insertions(+), 50 deletions(-) create mode 100644 packages/providers/src/ssejs.d.ts create mode 100644 packages/providers/src/subscription-client.ts diff --git a/packages/providers/codegen.json b/packages/providers/codegen.json index 0f48ea80586..d21c27157c3 100644 --- a/packages/providers/codegen.json +++ b/packages/providers/codegen.json @@ -7,7 +7,7 @@ "plugins": [ { "typescript": {} }, { "typescript-operations": {} }, - { "typescript-graphql-request": {} } + { "typescript-generic-sdk": {} } ], "config": { "scalars": { diff --git a/packages/providers/package.json b/packages/providers/package.json index 18d9898ec48..e0ea3d58e1f 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -43,7 +43,9 @@ "graphql-request": "^5.0.0", "graphql-tag": "^2.12.6", "ramda": "^0.29.0", - "tai64": "^1.0.0" + "tai64": "^1.0.0", + "eventsource": "^2.0.2", + "sse.js": "^1.0.0" }, "devDependencies": { "@fuel-ts/utils": "workspace:*", @@ -51,8 +53,9 @@ "@graphql-codegen/typescript": "^2.8.0", "@graphql-codegen/typescript-graphql-request": "^4.5.7", "@graphql-codegen/typescript-operations": "^2.5.5", - "@types/ramda": "^0.29.3", + "@graphql-codegen/typescript-generic-sdk": "^3.1.0", "get-graphql-schema": "^2.1.2", - "typescript": "^4.8.4" + "@types/ramda": "^0.29.3", + "@types/eventsource": "^1.1.13" } } diff --git a/packages/providers/src/operations.graphql b/packages/providers/src/operations.graphql index f92f7691c3e..5e14a6fdeaf 100644 --- a/packages/providers/src/operations.graphql +++ b/packages/providers/src/operations.graphql @@ -4,6 +4,31 @@ # generate `operations.ts` from this file. # Fragments + +fragment transactionStatusFragment on TransactionStatus { + type: __typename + ... on SubmittedStatus { + time + } + ... on SuccessStatus { + block { + id + } + time + programState { + returnType + data + } + } + ... on FailureStatus { + block { + id + } + time + reason + } +} + fragment transactionFragment on Transaction { id rawPayload @@ -12,27 +37,7 @@ fragment transactionFragment on Transaction { ...receiptFragment } status { - type: __typename - ... on SubmittedStatus { - time - } - ... on SuccessStatus { - block { - id - } - time - programState { - returnType - data - } - } - ... on FailureStatus { - block { - id - } - time - reason - } + ...transactionStatusFragment } } @@ -468,3 +473,15 @@ mutation produceBlocks( startTimestamp: $startTimestamp ) } + +subscription submitAndAwait($encodedTransaction: HexString!) { + submitAndAwait(tx: $encodedTransaction) { + ...transactionStatusFragment + } +} + +subscription statusChange($transactionId: TransactionId!) { + statusChange(id: $transactionId) { + ...transactionStatusFragment + } +} diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index acd8aa46e7b..656921515d4 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -13,6 +13,7 @@ import { import { checkFuelCoreVersionCompatibility } from '@fuel-ts/versions'; import type { BytesLike } from 'ethers'; import { getBytesCopy, hexlify, Network } from 'ethers'; +import { print } from 'graphql'; import { GraphQLClient } from 'graphql-request'; import { clone } from 'ramda'; @@ -349,7 +350,25 @@ export default class Provider { private createOperations(url: string, options: ProviderOptions = {}) { this.url = url; const gqlClient = new GraphQLClient(url, options.fetch ? { fetch: options.fetch } : undefined); - return getOperationsSdk(gqlClient); + + // @ts-expect-error This is due to this function being generic and us using multiple libraries. Its type is specified when calling a specific operation via provider.operations.xyz. + return getOperationsSdk((query, vars) => { + const isSubscription = + (query.definitions.find((x) => x.kind === 'OperationDefinition') as { operation: string }) + ?.operation === 'subscription'; + if (isSubscription) { + const q = print(query); + const evntSource = new EventSource(`${this.url}-sub`); + return gqlClient.request(query, vars); + + // return this.#subscriptionClient.iterate({ + // query: print(query), + // variables: vars as Record, + // }); + } + + return gqlClient.request(query, vars); + }); } /** diff --git a/packages/providers/src/ssejs.d.ts b/packages/providers/src/ssejs.d.ts new file mode 100644 index 00000000000..3d32ae6bb54 --- /dev/null +++ b/packages/providers/src/ssejs.d.ts @@ -0,0 +1,14 @@ +declare module 'sse.js' { + interface SSEOptions extends globalThis.RequestInit, EventSourceInit { + debug?: boolean; + } + + interface SSE extends EventSource {} + + declare let SSE: { + new (url: string | URL, options?: SSEOptions): SSE; + readonly CLOSED: number; + readonly CONNECTING: number; + readonly OPEN: number; + }; +} diff --git a/packages/providers/src/subscription-client.ts b/packages/providers/src/subscription-client.ts new file mode 100644 index 00000000000..8929c674145 --- /dev/null +++ b/packages/providers/src/subscription-client.ts @@ -0,0 +1,24 @@ +import type { SSEOptions } from 'sse.js'; +import { SSE } from 'sse.js'; + +export class Iterator implements AsyncIterableIterator, AsyncIterator { + constructor(url: string, options?: SSEOptions) { + const sse = new SSE(url); + } + + [Symbol.asyncIterator](): AsyncIterableIterator { + throw new Error('Method not implemented.'); + } + + next(...args: []): Promise> { + throw new Error('Method not implemented.'); + } + + return?(value?: TReturn | PromiseLike | undefined): Promise> { + throw new Error('Method not implemented.'); + } + + throw?(e?: Error): Promise> { + throw new Error('Method not implemented.'); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8847f2fe8c3..1c8875332d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -904,6 +904,9 @@ importers: ethers: specifier: ^6.7.1 version: 6.7.1 + eventsource: + specifier: ^2.0.2 + version: 2.0.2 graphql: specifier: ^16.6.0 version: 16.6.0 @@ -916,6 +919,9 @@ importers: ramda: specifier: ^0.29.0 version: 0.29.0 + sse.js: + specifier: ^1.0.0 + version: 1.0.0 tai64: specifier: ^1.0.0 version: 1.0.0 @@ -925,25 +931,28 @@ importers: version: link:../utils '@graphql-codegen/cli': specifier: ^2.13.7 - version: 2.13.7(@babel/core@7.22.5)(@types/node@16.18.34)(graphql@16.6.0)(ts-node@10.9.1)(typescript@4.9.5) + version: 2.13.7(@babel/core@7.22.5)(@types/node@16.18.34)(graphql@16.6.0)(ts-node@10.9.1)(typescript@5.1.6) '@graphql-codegen/typescript': specifier: ^2.8.0 version: 2.8.0(graphql@16.6.0) + '@graphql-codegen/typescript-generic-sdk': + specifier: ^3.1.0 + version: 3.1.0(graphql-tag@2.12.6)(graphql@16.6.0) '@graphql-codegen/typescript-graphql-request': specifier: ^4.5.7 - version: 4.5.7(graphql-request@5.0.0)(graphql-tag@2.12.6)(graphql@16.6.0) + version: 4.5.9(graphql-request@5.0.0)(graphql-tag@2.12.6)(graphql@16.6.0) '@graphql-codegen/typescript-operations': specifier: ^2.5.5 version: 2.5.5(graphql@16.6.0) + '@types/eventsource': + specifier: ^1.1.13 + version: 1.1.13 '@types/ramda': specifier: ^0.29.3 version: 0.29.3 get-graphql-schema: specifier: ^2.1.2 version: 2.1.2 - typescript: - specifier: ^4.8.4 - version: 4.9.5 packages/script: dependencies: @@ -3514,7 +3523,7 @@ packages: resolution: {integrity: sha512-MXtNDk0WXONIrDJOlk07+X7GegpCz2hfbAgSIWycOD0th2z1GndvMqBryiw/pTVDHLnHe+5TGIODLsprI4RiEw==} dev: false - /@graphql-codegen/cli@2.13.7(@babel/core@7.22.5)(@types/node@16.18.34)(graphql@16.6.0)(ts-node@10.9.1)(typescript@4.9.5): + /@graphql-codegen/cli@2.13.7(@babel/core@7.22.5)(@types/node@16.18.34)(graphql@16.6.0)(ts-node@10.9.1)(typescript@5.1.6): resolution: {integrity: sha512-Rpk4WWrDgkDoVELftBr7/74MPiYmCITEF2+AWmyZZ2xzaC9cO2PqzZ+OYDEBNWD6UEk0RrIfVSa+slDKjhY59w==} hasBin: true peerDependencies: @@ -3540,11 +3549,11 @@ packages: chalk: 4.1.2 chokidar: 3.5.3 cosmiconfig: 7.1.0 - cosmiconfig-typescript-loader: 4.1.1(@types/node@16.18.34)(cosmiconfig@7.1.0)(ts-node@10.9.1)(typescript@4.9.5) + cosmiconfig-typescript-loader: 4.1.1(@types/node@16.18.34)(cosmiconfig@7.1.0)(ts-node@10.9.1)(typescript@5.1.6) debounce: 1.2.1 detect-indent: 6.1.0 graphql: 16.6.0 - graphql-config: 4.3.6(@types/node@16.18.34)(graphql@16.6.0)(typescript@4.9.5) + graphql-config: 4.3.6(@types/node@16.18.34)(graphql@16.6.0)(typescript@5.1.6) inquirer: 8.2.5 is-glob: 4.0.3 json-to-pretty-yaml: 1.2.2 @@ -3622,15 +3631,32 @@ packages: tslib: 2.4.1 dev: true - /@graphql-codegen/typescript-graphql-request@4.5.7(graphql-request@5.0.0)(graphql-tag@2.12.6)(graphql@16.6.0): - resolution: {integrity: sha512-1YPaCO+0q5z0Um6Om+5LMWdB8+WQxda8eXRXwy0dqSGRy9X5HTZz/pxqaTgy76yMtPBxq1UNa7lruBTzszHhJg==} + /@graphql-codegen/typescript-generic-sdk@3.1.0(graphql-tag@2.12.6)(graphql@16.6.0): + resolution: {integrity: sha512-nQZi/YGRI1+qCZZsh0V5nz6+hCHSN4OU9tKyOTDsEPyDFnGEukDuRdCH2IZasGn22a3Iu5TUDkgp5w9wEQwGmg==} peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - graphql-request: ^3.4.0 || ^4.0.0 || ^5.0.0 graphql-tag: ^2.0.0 dependencies: - '@graphql-codegen/plugin-helpers': 2.7.2(graphql@16.6.0) - '@graphql-codegen/visitor-plugin-common': 2.13.0(graphql@16.6.0) + '@graphql-codegen/plugin-helpers': 3.1.2(graphql@16.6.0) + '@graphql-codegen/visitor-plugin-common': 2.13.1(graphql@16.6.0) + auto-bind: 4.0.0 + graphql: 16.6.0 + graphql-tag: 2.12.6(graphql@16.6.0) + tslib: 2.4.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: true + + /@graphql-codegen/typescript-graphql-request@4.5.9(graphql-request@5.0.0)(graphql-tag@2.12.6)(graphql@16.6.0): + resolution: {integrity: sha512-Vtv5qymUXcR4UFdHOlJHzK5TN+CZUwMwFDGb3n4Gjcr4yln1BWbUb7DXgD0GHzpXwDIj5G2XmJnFtr0jihBfrg==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-request: ^3.4.0 || ^4.0.0 || ~5.0.0 || ~5.1.0 + graphql-tag: ^2.0.0 + dependencies: + '@graphql-codegen/plugin-helpers': 3.1.2(graphql@16.6.0) + '@graphql-codegen/visitor-plugin-common': 2.13.1(graphql@16.6.0) auto-bind: 4.0.0 graphql: 16.6.0 graphql-request: 5.0.0(graphql@16.6.0) @@ -3694,6 +3720,27 @@ packages: - supports-color dev: true + /@graphql-codegen/visitor-plugin-common@2.13.1(graphql@16.6.0): + resolution: {integrity: sha512-mD9ufZhDGhyrSaWQGrU1Q1c5f01TeWtSWy/cDwXYjJcHIj1Y/DG2x0tOflEfCvh5WcnmHNIw4lzDsg1W7iFJEg==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + dependencies: + '@graphql-codegen/plugin-helpers': 2.7.2(graphql@16.6.0) + '@graphql-tools/optimize': 1.4.0(graphql@16.6.0) + '@graphql-tools/relay-operation-optimizer': 6.5.18(graphql@16.6.0) + '@graphql-tools/utils': 8.13.1(graphql@16.6.0) + auto-bind: 4.0.0 + change-case-all: 1.0.14 + dependency-graph: 0.11.0 + graphql: 16.6.0 + graphql-tag: 2.12.6(graphql@16.6.0) + parse-filepath: 1.0.2 + tslib: 2.4.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: true + /@graphql-tools/apollo-engine-loader@7.3.26(graphql@16.6.0): resolution: {integrity: sha512-h1vfhdJFjnCYn9b5EY1Z91JTF0KB3hHVJNQIsiUV2mpQXZdeOXQoaWeYEKaiI5R6kwBw5PP9B0fv3jfUIG8LyQ==} peerDependencies: @@ -5404,6 +5451,10 @@ packages: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} dev: false + /@types/eventsource@1.1.13: + resolution: {integrity: sha512-Jd1y/YP7etWQMM1TWxI40VDGQepmYWRp+lICHHAADR5ogdwbIfNQpVGXjsDT3p6piXXY80DVV0BQN0PA+emkTA==} + dev: true + /@types/express-serve-static-core@4.17.35: resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} dependencies: @@ -7779,7 +7830,7 @@ packages: '@iarna/toml': 2.2.5 dev: true - /cosmiconfig-typescript-loader@4.1.1(@types/node@16.18.34)(cosmiconfig@7.0.1)(ts-node@10.9.1)(typescript@4.9.5): + /cosmiconfig-typescript-loader@4.1.1(@types/node@16.18.34)(cosmiconfig@7.0.1)(ts-node@10.9.1)(typescript@5.1.6): resolution: {integrity: sha512-9DHpa379Gp0o0Zefii35fcmuuin6q92FnLDffzdZ0l9tVd3nEobG3O+MZ06+kuBvFTSVScvNb/oHA13Nd4iipg==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -7790,11 +7841,11 @@ packages: dependencies: '@types/node': 16.18.34 cosmiconfig: 7.0.1 - ts-node: 10.9.1(@types/node@16.18.34)(typescript@4.9.5) - typescript: 4.9.5 + ts-node: 10.9.1(@types/node@16.18.34)(typescript@5.1.6) + typescript: 5.1.6 dev: true - /cosmiconfig-typescript-loader@4.1.1(@types/node@16.18.34)(cosmiconfig@7.1.0)(ts-node@10.9.1)(typescript@4.9.5): + /cosmiconfig-typescript-loader@4.1.1(@types/node@16.18.34)(cosmiconfig@7.1.0)(ts-node@10.9.1)(typescript@5.1.6): resolution: {integrity: sha512-9DHpa379Gp0o0Zefii35fcmuuin6q92FnLDffzdZ0l9tVd3nEobG3O+MZ06+kuBvFTSVScvNb/oHA13Nd4iipg==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -7805,8 +7856,8 @@ packages: dependencies: '@types/node': 16.18.34 cosmiconfig: 7.1.0 - ts-node: 10.9.1(@types/node@16.18.34)(typescript@4.9.5) - typescript: 4.9.5 + ts-node: 10.9.1(@types/node@16.18.34)(typescript@5.1.6) + typescript: 5.1.6 dev: true /cosmiconfig@6.0.0: @@ -9208,6 +9259,11 @@ packages: engines: {node: '>=0.8.x'} dev: false + /eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + dev: false + /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -9851,7 +9907,7 @@ packages: /grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} - /graphql-config@4.3.6(@types/node@16.18.34)(graphql@16.6.0)(typescript@4.9.5): + /graphql-config@4.3.6(@types/node@16.18.34)(graphql@16.6.0)(typescript@5.1.6): resolution: {integrity: sha512-i7mAPwc0LAZPnYu2bI8B6yXU5820Wy/ArvmOseDLZIu0OU1UTULEuexHo6ZcHXeT9NvGGaUPQZm8NV3z79YydA==} engines: {node: '>= 10.0.0'} peerDependencies: @@ -9865,11 +9921,11 @@ packages: '@graphql-tools/utils': 8.13.1(graphql@16.6.0) cosmiconfig: 7.0.1 cosmiconfig-toml-loader: 1.0.0 - cosmiconfig-typescript-loader: 4.1.1(@types/node@16.18.34)(cosmiconfig@7.0.1)(ts-node@10.9.1)(typescript@4.9.5) + cosmiconfig-typescript-loader: 4.1.1(@types/node@16.18.34)(cosmiconfig@7.0.1)(ts-node@10.9.1)(typescript@5.1.6) graphql: 16.6.0 minimatch: 4.2.1 string-env-interpolation: 1.0.1 - ts-node: 10.9.1(@types/node@16.18.34)(typescript@4.9.5) + ts-node: 10.9.1(@types/node@16.18.34)(typescript@5.1.6) tslib: 2.6.0 transitivePeerDependencies: - '@swc/core' @@ -15473,6 +15529,10 @@ packages: /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + /sse.js@1.0.0: + resolution: {integrity: sha512-oYYx3haJBwXvuMzNEoZGQb0dsqhJJO9bgDs5VTppocoUYy/CftkmFjY9+R6H8tSotjmwuhYdMDJf+MMSDm8SNQ==} + dev: false + /stable@0.1.8: resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' @@ -16173,7 +16233,7 @@ packages: resolution: {integrity: sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==} dev: true - /ts-node@10.9.1(@types/node@16.18.34)(typescript@4.9.5): + /ts-node@10.9.1(@types/node@16.18.34)(typescript@5.1.6): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -16199,7 +16259,7 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 4.9.5 + typescript: 5.1.6 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true @@ -16491,6 +16551,7 @@ packages: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} hasBin: true + dev: false /typescript@5.0.2: resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==} From a1a4567bbd953a3af943c8264e72fceb8988f80b Mon Sep 17 00:00:00 2001 From: nedsalk Date: Mon, 23 Oct 2023 16:56:15 +0200 Subject: [PATCH 02/51] mostly implemented subscriptions --- .eslintrc.js | 6 ++ .vscode/settings.json | 1 - package.json | 5 -- packages/errors/src/error-codes.ts | 1 + .../src/transaction-response.test.ts | 65 +++++++++++++++---- packages/providers/package.json | 8 +-- packages/providers/src/provider.ts | 20 ++---- packages/providers/src/ssejs.d.ts | 14 ---- packages/providers/src/subscriber.ts | 56 ++++++++++++++++ packages/providers/src/subscription-client.ts | 24 ------- .../transaction-response.ts | 23 +++++-- pnpm-lock.yaml | 59 ++--------------- 12 files changed, 148 insertions(+), 134 deletions(-) delete mode 100644 packages/providers/src/ssejs.d.ts create mode 100644 packages/providers/src/subscriber.ts delete mode 100644 packages/providers/src/subscription-client.ts diff --git a/.eslintrc.js b/.eslintrc.js index e30f62a7dae..309b0cab165 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,6 +22,12 @@ module.exports = { }, }, rules: { + 'no-restricted-syntax': [ + 'off', + { + selector: 'ForOfStatement', + }, + ], // Disable error on devDependencies importing since this isn't a TS library 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 'no-await-in-loop': 0, diff --git a/.vscode/settings.json b/.vscode/settings.json index 9265bce8080..3bab6ff3d03 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,5 @@ { "editor.defaultFormatter": "esbenp.prettier-vscode", - "prettier.prettierPath": "./node_modules/prettier", "prettier.configPath": ".prettierrc", "editor.formatOnSave": true, "editor.codeActionsOnSave": { diff --git a/package.json b/package.json index 6644bfa6a58..d0fda33893d 100644 --- a/package.json +++ b/package.json @@ -89,10 +89,5 @@ "tsx": "^3.12.7", "turbo": "^1.8.8", "typescript": "~5.1.6" - }, - "pnpm": { - "overrides": { - "cross-fetch": "4.0.0" - } } } diff --git a/packages/errors/src/error-codes.ts b/packages/errors/src/error-codes.ts index ea1f15106ee..b0661e1cc15 100644 --- a/packages/errors/src/error-codes.ts +++ b/packages/errors/src/error-codes.ts @@ -46,6 +46,7 @@ export enum ErrorCode { CONVERTING_FAILED = 'converting-error', ELEMENT_NOT_FOUND = 'element-not-found', MISSING_REQUIRED_PARAMETER = 'missing-required-parameter', + INVALID_REQUEST = 'invalid-request', // transaction GAS_PRICE_TOO_LOW = 'gas-price-too-low', diff --git a/packages/fuel-gauge/src/transaction-response.test.ts b/packages/fuel-gauge/src/transaction-response.test.ts index 8ab17619992..28b26a7a3bc 100644 --- a/packages/fuel-gauge/src/transaction-response.test.ts +++ b/packages/fuel-gauge/src/transaction-response.test.ts @@ -1,6 +1,14 @@ -import { generateTestWallet } from '@fuel-ts/wallet/test-utils'; -import type { BN, WalletUnlocked } from 'fuels'; -import { BaseAssetId, FUEL_NETWORK_URL, Provider, TransactionResponse, Wallet } from 'fuels'; +import { generateTestWallet, launchNode } from '@fuel-ts/wallet/test-utils'; +import type { BN } from 'fuels'; +import { + BaseAssetId, + FUEL_NETWORK_URL, + Provider, + TransactionResponse, + Wallet, + randomBytes, + WalletUnlocked, +} from 'fuels'; describe('TransactionSummary', () => { let provider: Provider; @@ -74,26 +82,57 @@ describe('TransactionSummary', () => { expect(response.gqlTransaction?.id).toBe(transactionId); }); - it('should ensure waitForResult always waits for the transaction to be processed', async () => { - const destination = Wallet.generate({ - provider, + // it('should ensure waitForResult always waits for the transaction to be processed', async () => { + // const destination = Wallet.generate({ + // provider, + // }); + + // const { id: transactionId } = await adminWallet.transfer( + // destination.address, + // 100, + // BaseAssetId, + // { gasPrice } + // ); + + // const response = new TransactionResponse(transactionId, provider); + + // expect(response.gqlTransaction).toBeUndefined(); + + // await response.waitForResult(); + + // expect(response.gqlTransaction?.status?.type).toBeDefined(); + // expect(response.gqlTransaction?.status?.type).not.toEqual('SubmittedStatus'); + // expect(response.gqlTransaction?.id).toBe(transactionId); + // }); + + it('[true test] should ensure waitForResult always waits for the transaction to be processed', async () => { + const { cleanup, ip, port } = await launchNode({ + args: ['--poa-interval-period', '10s'], }); + const nodeProvider = await Provider.create(`http://${ip}:${port}/graphql`); - const { id: transactionId } = await adminWallet.transfer( + const genesisWallet = new WalletUnlocked( + process.env.GENESIS_SECRET || randomBytes(32), + nodeProvider + ); + + const destination = Wallet.generate({ provider: nodeProvider }); + + const { id: transactionId } = await genesisWallet.transfer( destination.address, 100, BaseAssetId, { gasPrice } ); + const response = new TransactionResponse('asdsadflk3jeh', nodeProvider); - const response = new TransactionResponse(transactionId, provider); - - expect(response.gqlTransaction).toBeUndefined(); + // expect(response.gqlTransaction?.status?.type).toBe('SubmittedStatus'); await response.waitForResult(); - expect(response.gqlTransaction?.status?.type).toBeDefined(); - expect(response.gqlTransaction?.status?.type).not.toEqual('SubmittedStatus'); + expect(response.gqlTransaction?.status?.type).toEqual('SuccessStatus'); expect(response.gqlTransaction?.id).toBe(transactionId); - }); + + cleanup(); + }, 25000); }); diff --git a/packages/providers/package.json b/packages/providers/package.json index b5b634d928a..f0c8ab00ef4 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -43,19 +43,15 @@ "graphql-request": "^5.0.0", "graphql-tag": "^2.12.6", "ramda": "^0.29.0", - "tai64": "^1.0.0", - "eventsource": "^2.0.2", - "sse.js": "^1.0.0" + "tai64": "^1.0.0" }, "devDependencies": { "@fuel-ts/utils": "workspace:*", "@graphql-codegen/cli": "^2.13.7", "@graphql-codegen/typescript": "^2.8.0", - "@graphql-codegen/typescript-graphql-request": "^4.5.7", "@graphql-codegen/typescript-operations": "^2.5.5", "@graphql-codegen/typescript-generic-sdk": "^3.1.0", "get-graphql-schema": "^2.1.2", - "@types/ramda": "^0.29.3", - "@types/eventsource": "^1.1.13" + "@types/ramda": "^0.29.3" } } diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index bde3da12509..8f87b2730c2 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -13,7 +13,6 @@ import { import { checkFuelCoreVersionCompatibility } from '@fuel-ts/versions'; import type { BytesLike } from 'ethers'; import { getBytesCopy, hexlify, Network } from 'ethers'; -import { print } from 'graphql'; import { GraphQLClient } from 'graphql-request'; import { clone } from 'ramda'; @@ -28,6 +27,7 @@ import { coinQuantityfy } from './coin-quantity'; import { MemoryCache } from './memory-cache'; import type { Message, MessageCoin, MessageProof, MessageStatus } from './message'; import type { ExcludeResourcesOption, Resource } from './resource'; +import { Subscriber } from './subscriber'; import type { TransactionRequestLike, TransactionRequest, @@ -349,25 +349,19 @@ export default class Provider { */ private createOperations(url: string, options: ProviderOptions = {}) { this.url = url; - const gqlClient = new GraphQLClient(url, options.fetch ? { fetch: options.fetch } : undefined); + + const fetchFn = (options.fetch as typeof fetch) ?? fetch; + const gqlClient = new GraphQLClient(url, { fetch: fetchFn }); // @ts-expect-error This is due to this function being generic and us using multiple libraries. Its type is specified when calling a specific operation via provider.operations.xyz. return getOperationsSdk((query, vars) => { const isSubscription = (query.definitions.find((x) => x.kind === 'OperationDefinition') as { operation: string }) ?.operation === 'subscription'; - if (isSubscription) { - const q = print(query); - const evntSource = new EventSource(`${this.url}-sub`); - return gqlClient.request(query, vars); - - // return this.#subscriptionClient.iterate({ - // query: print(query), - // variables: vars as Record, - // }); - } - return gqlClient.request(query, vars); + return isSubscription + ? new Subscriber(this.url, query, vars as Record, fetchFn).subscribe() + : gqlClient.request(query, vars); }); } diff --git a/packages/providers/src/ssejs.d.ts b/packages/providers/src/ssejs.d.ts deleted file mode 100644 index 3d32ae6bb54..00000000000 --- a/packages/providers/src/ssejs.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare module 'sse.js' { - interface SSEOptions extends globalThis.RequestInit, EventSourceInit { - debug?: boolean; - } - - interface SSE extends EventSource {} - - declare let SSE: { - new (url: string | URL, options?: SSEOptions): SSE; - readonly CLOSED: number; - readonly CONNECTING: number; - readonly OPEN: number; - }; -} diff --git a/packages/providers/src/subscriber.ts b/packages/providers/src/subscriber.ts new file mode 100644 index 00000000000..942c76d7dc1 --- /dev/null +++ b/packages/providers/src/subscriber.ts @@ -0,0 +1,56 @@ +import { FuelError } from '@fuel-ts/errors'; +import type { DocumentNode } from 'graphql'; +import { print } from 'graphql'; + +export class Subscriber { + private request: Promise; + private reader?: ReadableStreamDefaultReader; + + constructor( + url: string, + query: DocumentNode, + variables: Record | undefined, + fetchFn: typeof fetch + ) { + const queryString = print(query); + const requestBody = { + query: queryString, + variables, + }; + + this.request = fetchFn(`${url}-sub`, { + method: 'POST', + body: JSON.stringify(requestBody), + keepalive: true, + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream', + }, + }); + } + + async *subscribe() { + if (this.reader === undefined) { + const response = await this.request; + this.reader = response.body!.getReader(); + } + + while (true) { + const { value, done } = await this.reader.read(); + const text = new TextDecoder().decode(value); + if (text.startsWith('data:')) { + const { data, errors } = JSON.parse(text.split('data:')[1]); + if (Array.isArray(errors)) { + this.reader.releaseLock(); + await this.reader.cancel(); + throw new FuelError( + FuelError.CODES.INVALID_REQUEST, + errors.map((x) => x.message).join('\n\n') + ); + } + yield data; + if (done) break; + } + } + } +} diff --git a/packages/providers/src/subscription-client.ts b/packages/providers/src/subscription-client.ts deleted file mode 100644 index 8929c674145..00000000000 --- a/packages/providers/src/subscription-client.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { SSEOptions } from 'sse.js'; -import { SSE } from 'sse.js'; - -export class Iterator implements AsyncIterableIterator, AsyncIterator { - constructor(url: string, options?: SSEOptions) { - const sse = new SSE(url); - } - - [Symbol.asyncIterator](): AsyncIterableIterator { - throw new Error('Method not implemented.'); - } - - next(...args: []): Promise> { - throw new Error('Method not implemented.'); - } - - return?(value?: TReturn | PromiseLike | undefined): Promise> { - throw new Error('Method not implemented.'); - } - - throw?(e?: Error): Promise> { - throw new Error('Method not implemented.'); - } -} diff --git a/packages/providers/src/transaction-response/transaction-response.ts b/packages/providers/src/transaction-response/transaction-response.ts index 770ccb31a2c..3f4587ed4aa 100644 --- a/packages/providers/src/transaction-response/transaction-response.ts +++ b/packages/providers/src/transaction-response/transaction-response.ts @@ -133,7 +133,14 @@ export class TransactionResponse { }); if (!response.transaction) { - await this.sleepBasedOnAttempts(++this.fetchAttempts); + for await (const res of this.provider.operations.statusChange({ + transactionId: this.id, + })) { + if (res.statusChange) { + break; + } + } + return this.fetch(); } @@ -203,13 +210,17 @@ export class TransactionResponse { async waitForResult( contractsAbiMap?: AbiMap ): Promise> { + for await (const res of this.provider.operations.statusChange({ + transactionId: this.id, + })) { + if (res.statusChange.type !== 'SubmittedStatus') break; + } await this.fetch(); + // if (this.gqlTransaction?.status?.type === 'SubmittedStatus') { + // await this.sleepBasedOnAttempts(++this.resultAttempts); - if (this.gqlTransaction?.status?.type === 'SubmittedStatus') { - await this.sleepBasedOnAttempts(++this.resultAttempts); - - return this.waitForResult(contractsAbiMap); - } + // return this.waitForResult(contractsAbiMap); + // } const transactionSummary = await this.getTransactionSummary(contractsAbiMap); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 140f82c8a37..47b6f6be4c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,9 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - cross-fetch: 4.0.0 - importers: .: @@ -907,9 +904,6 @@ importers: ethers: specifier: ^6.7.1 version: 6.7.1 - eventsource: - specifier: ^2.0.2 - version: 2.0.2 graphql: specifier: ^16.6.0 version: 16.6.0 @@ -922,9 +916,6 @@ importers: ramda: specifier: ^0.29.0 version: 0.29.0 - sse.js: - specifier: ^1.0.0 - version: 1.0.0 tai64: specifier: ^1.0.0 version: 1.0.0 @@ -941,15 +932,9 @@ importers: '@graphql-codegen/typescript-generic-sdk': specifier: ^3.1.0 version: 3.1.0(graphql-tag@2.12.6)(graphql@16.6.0) - '@graphql-codegen/typescript-graphql-request': - specifier: ^4.5.7 - version: 4.5.9(graphql-request@5.0.0)(graphql-tag@2.12.6)(graphql@16.6.0) '@graphql-codegen/typescript-operations': specifier: ^2.5.5 version: 2.5.5(graphql@16.6.0) - '@types/eventsource': - specifier: ^1.1.13 - version: 1.1.13 '@types/ramda': specifier: ^0.29.3 version: 0.29.3 @@ -3651,25 +3636,6 @@ packages: - supports-color dev: true - /@graphql-codegen/typescript-graphql-request@4.5.9(graphql-request@5.0.0)(graphql-tag@2.12.6)(graphql@16.6.0): - resolution: {integrity: sha512-Vtv5qymUXcR4UFdHOlJHzK5TN+CZUwMwFDGb3n4Gjcr4yln1BWbUb7DXgD0GHzpXwDIj5G2XmJnFtr0jihBfrg==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - graphql-request: ^3.4.0 || ^4.0.0 || ~5.0.0 || ~5.1.0 - graphql-tag: ^2.0.0 - dependencies: - '@graphql-codegen/plugin-helpers': 3.1.2(graphql@16.6.0) - '@graphql-codegen/visitor-plugin-common': 2.13.1(graphql@16.6.0) - auto-bind: 4.0.0 - graphql: 16.6.0 - graphql-request: 5.0.0(graphql@16.6.0) - graphql-tag: 2.12.6(graphql@16.6.0) - tslib: 2.4.1 - transitivePeerDependencies: - - encoding - - supports-color - dev: true - /@graphql-codegen/typescript-operations@2.5.5(graphql@16.6.0): resolution: {integrity: sha512-rH15UA34MRf6cITfvt2EkSEaC/8ULvgMg5kzun6895oucA8PFyAFJaQzcjk9UraeD3ddMu56OKhZGdxd0JWfKw==} peerDependencies: @@ -5454,10 +5420,6 @@ packages: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} dev: false - /@types/eventsource@1.1.13: - resolution: {integrity: sha512-Jd1y/YP7etWQMM1TWxI40VDGQepmYWRp+lICHHAADR5ogdwbIfNQpVGXjsDT3p6piXXY80DVV0BQN0PA+emkTA==} - dev: true - /@types/express-serve-static-core@4.17.35: resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} dependencies: @@ -7899,8 +7861,8 @@ packages: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true - /cross-fetch@4.0.0: - resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + /cross-fetch@3.1.8: + resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} dependencies: node-fetch: 2.6.12 transitivePeerDependencies: @@ -9262,11 +9224,6 @@ packages: engines: {node: '>=0.8.x'} dev: false - /eventsource@2.0.2: - resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} - engines: {node: '>=12.0.0'} - dev: false - /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -9369,6 +9326,7 @@ packages: /extract-files@9.0.0: resolution: {integrity: sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==} engines: {node: ^10.17.0 || ^12.0.0 || >= 13.7.0} + dev: false /fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} @@ -9443,7 +9401,7 @@ packages: /fbjs@3.0.5: resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} dependencies: - cross-fetch: 4.0.0 + cross-fetch: 3.1.8 fbjs-css-vars: 1.0.2 loose-envify: 1.4.0 object-assign: 4.1.1 @@ -9946,12 +9904,13 @@ packages: graphql: 14 - 16 dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.6.0) - cross-fetch: 4.0.0 + cross-fetch: 3.1.8 extract-files: 9.0.0 form-data: 3.0.1 graphql: 16.6.0 transitivePeerDependencies: - encoding + dev: false /graphql-request@6.1.0(graphql@16.6.0): resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} @@ -9959,7 +9918,7 @@ packages: graphql: 14 - 16 dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.6.0) - cross-fetch: 4.0.0 + cross-fetch: 3.1.8 graphql: 16.6.0 transitivePeerDependencies: - encoding @@ -15532,10 +15491,6 @@ packages: /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - /sse.js@1.0.0: - resolution: {integrity: sha512-oYYx3haJBwXvuMzNEoZGQb0dsqhJJO9bgDs5VTppocoUYy/CftkmFjY9+R6H8tSotjmwuhYdMDJf+MMSDm8SNQ==} - dev: false - /stable@0.1.8: resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' From 450cfef8f52d0c31e9d8f6d35f329e828d1c4adc Mon Sep 17 00:00:00 2001 From: nedsalk Date: Tue, 24 Oct 2023 08:15:47 +0200 Subject: [PATCH 03/51] checkpoint --- packages/providers/src/provider.ts | 2 +- packages/providers/src/subscriber.ts | 35 +++++++++++++++++++++------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index 8f87b2730c2..85e6e7799fb 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -360,7 +360,7 @@ export default class Provider { ?.operation === 'subscription'; return isSubscription - ? new Subscriber(this.url, query, vars as Record, fetchFn).subscribe() + ? new Subscriber({url: this.url, query, variables: vars as Record, fetchFn}).subscribe() : gqlClient.request(query, vars); }); } diff --git a/packages/providers/src/subscriber.ts b/packages/providers/src/subscriber.ts index 942c76d7dc1..9ec7730c0fc 100644 --- a/packages/providers/src/subscriber.ts +++ b/packages/providers/src/subscriber.ts @@ -2,24 +2,33 @@ import { FuelError } from '@fuel-ts/errors'; import type { DocumentNode } from 'graphql'; import { print } from 'graphql'; +type SubscriberOptions = { + url: string; + query: DocumentNode; + variables?: Record; + fetchFn: typeof fetch; + abortController?: AbortController; +}; export class Subscriber { private request: Promise; private reader?: ReadableStreamDefaultReader; - - constructor( - url: string, - query: DocumentNode, - variables: Record | undefined, - fetchFn: typeof fetch - ) { + private abortController: AbortController; + constructor({ + url, + query, + variables, + fetchFn, + abortController = new AbortController(), + }: SubscriberOptions) { const queryString = print(query); const requestBody = { query: queryString, variables, }; - + this.abortController = abortController; this.request = fetchFn(`${url}-sub`, { method: 'POST', + signal: this.abortController.signal, body: JSON.stringify(requestBody), keepalive: true, headers: { @@ -28,6 +37,10 @@ export class Subscriber { }, }); } + async sub() { + const generator = this.subscribe(); + generator.return = () => {}; + } async *subscribe() { if (this.reader === undefined) { @@ -41,8 +54,12 @@ export class Subscriber { if (text.startsWith('data:')) { const { data, errors } = JSON.parse(text.split('data:')[1]); if (Array.isArray(errors)) { - this.reader.releaseLock(); await this.reader.cancel(); + // @ts-expect-error to dispose + this.request = null; + this.reader = undefined; + + this.abortController.abort(); throw new FuelError( FuelError.CODES.INVALID_REQUEST, errors.map((x) => x.message).join('\n\n') From c34c3b7c757c7a0d1d0d7b5ad669a781afd26408 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 14:22:05 +0200 Subject: [PATCH 04/51] feat: subscriptions work --- .../src/transaction-response.test.ts | 55 +++++++------- .../providers/src/fuel-graphql-subscriber.ts | 66 +++++++++++++++++ packages/providers/src/provider.ts | 58 ++++++++++++++- packages/providers/src/subscriber.ts | 73 ------------------- .../transaction-response.ts | 27 +------ 5 files changed, 150 insertions(+), 129 deletions(-) create mode 100644 packages/providers/src/fuel-graphql-subscriber.ts delete mode 100644 packages/providers/src/subscriber.ts diff --git a/packages/fuel-gauge/src/transaction-response.test.ts b/packages/fuel-gauge/src/transaction-response.test.ts index 28b26a7a3bc..a6e8477196e 100644 --- a/packages/fuel-gauge/src/transaction-response.test.ts +++ b/packages/fuel-gauge/src/transaction-response.test.ts @@ -1,3 +1,5 @@ +import { FuelError } from '@fuel-ts/errors'; +import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; import { generateTestWallet, launchNode } from '@fuel-ts/wallet/test-utils'; import type { BN } from 'fuels'; import { @@ -82,30 +84,7 @@ describe('TransactionSummary', () => { expect(response.gqlTransaction?.id).toBe(transactionId); }); - // it('should ensure waitForResult always waits for the transaction to be processed', async () => { - // const destination = Wallet.generate({ - // provider, - // }); - - // const { id: transactionId } = await adminWallet.transfer( - // destination.address, - // 100, - // BaseAssetId, - // { gasPrice } - // ); - - // const response = new TransactionResponse(transactionId, provider); - - // expect(response.gqlTransaction).toBeUndefined(); - - // await response.waitForResult(); - - // expect(response.gqlTransaction?.status?.type).toBeDefined(); - // expect(response.gqlTransaction?.status?.type).not.toEqual('SubmittedStatus'); - // expect(response.gqlTransaction?.id).toBe(transactionId); - // }); - - it('[true test] should ensure waitForResult always waits for the transaction to be processed', async () => { + it('should ensure waitForResult always waits for the transaction to be processed', async () => { const { cleanup, ip, port } = await launchNode({ args: ['--poa-interval-period', '10s'], }); @@ -124,9 +103,9 @@ describe('TransactionSummary', () => { BaseAssetId, { gasPrice } ); - const response = new TransactionResponse('asdsadflk3jeh', nodeProvider); + const response = await TransactionResponse.create(transactionId, nodeProvider); - // expect(response.gqlTransaction?.status?.type).toBe('SubmittedStatus'); + expect(response.gqlTransaction?.status?.type).toBe('SubmittedStatus'); await response.waitForResult(); @@ -135,4 +114,28 @@ describe('TransactionSummary', () => { cleanup(); }, 25000); + + it('ensure that an invalid request throws and does not hold test runner (closes all handles)', async () => { + const { cleanup, ip, port } = await launchNode({}); + const nodeProvider = await Provider.create(`http://${ip}:${port}/graphql`); + + const response = new TransactionResponse('asdsadflk3jeh', nodeProvider); + + await expectToThrowFuelError(() => response.waitForResult(), { + code: FuelError.CODES.INVALID_REQUEST, + }); + + await expectToThrowFuelError( + async () => { + for await (const value of nodeProvider.operations.statusChange({ + transactionId: 'asdfkljer', + })) { + console.log(value); + } + }, + + { code: FuelError.CODES.INVALID_REQUEST } + ); + cleanup(); + }); }); diff --git a/packages/providers/src/fuel-graphql-subscriber.ts b/packages/providers/src/fuel-graphql-subscriber.ts new file mode 100644 index 00000000000..cb88625657c --- /dev/null +++ b/packages/providers/src/fuel-graphql-subscriber.ts @@ -0,0 +1,66 @@ +import { FuelError } from '@fuel-ts/errors'; +import type { DocumentNode } from 'graphql'; +import { print } from 'graphql'; + +type FuelGraphQLSubscriberOptions = { + url: string; + query: DocumentNode; + variables?: Record; + fetchFn: typeof fetch; + abortController?: AbortController; +}; + +export async function* fuelGraphQLSubscriber({ + url, + variables, + query, + fetchFn, +}: FuelGraphQLSubscriberOptions) { + const streamReader = await fetchFn(`${url}-sub`, { + method: 'POST', + keepalive: true, + body: JSON.stringify({ + query: print(query), + variables, + }), + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream', + }, + }).then((x) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const reader = x.body!.pipeThrough(new TextDecoderStream()).getReader(); + + return new ReadableStream({ + start(controller) { + reader.read().then(function push(result) { + const { done, value } = result; + if (done) { + controller.close(); + return; + } + if (value.startsWith('data:')) { + const { data, errors } = JSON.parse(value.split('data:')[1]); + if (Array.isArray(errors)) { + controller.enqueue( + new FuelError( + FuelError.CODES.INVALID_REQUEST, + errors.map((err) => err.message).join('\n\n') + ) + ); + } else controller.enqueue(data); + } + + reader.read().then(push); + }); + }, + }).getReader(); + }); + + for (;;) { + const { value, done } = await streamReader.read(); + if (value instanceof FuelError) throw value; + yield value; + if (done) break; + } +} diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index 85e6e7799fb..9919e39f7c4 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -24,10 +24,10 @@ import type { import type { Coin } from './coin'; import type { CoinQuantity, CoinQuantityLike } from './coin-quantity'; import { coinQuantityfy } from './coin-quantity'; +import { fuelGraphQLSubscriber } from './fuel-graphql-subscriber'; import { MemoryCache } from './memory-cache'; import type { Message, MessageCoin, MessageProof, MessageStatus } from './message'; import type { ExcludeResourcesOption, Resource } from './resource'; -import { Subscriber } from './subscriber'; import type { TransactionRequestLike, TransactionRequest, @@ -359,9 +359,59 @@ export default class Provider { (query.definitions.find((x) => x.kind === 'OperationDefinition') as { operation: string }) ?.operation === 'subscription'; - return isSubscription - ? new Subscriber({url: this.url, query, variables: vars as Record, fetchFn}).subscribe() - : gqlClient.request(query, vars); + if (isSubscription) { + return fuelGraphQLSubscriber({ + url: this.url, + query, + fetchFn, + variables: vars as Record, + }); + // return subscriber((abortController) => + // fetchFn(`${url}-sub`, { + // method: 'POST', + // signal: abortController.signal, + // body: JSON.stringify(requestBody), + // headers: { + // 'Content-Type': 'application/json', + // Accept: 'text/event-stream', + // }, + // }) + // .then((x) => { + // const reader = x.body!.getReader(); + + // let text = ''; + // // @ts-expect-error asd + // return reader.read().then(function process(result) { + // text += new TextDecoder().decode(result.value); + // if (!result.done) return reader.read().then(process); + // if (!text.startsWith('data:')) { + // text = ''; + // return reader.read().then(process); + // } + + // const { data, errors } = JSON.parse(text.split('data:')[1]); + // if (!Array.isArray(errors)) return data; + + // return reader + // .cancel() + // .then(() => + // Promise.reject( + // new FuelError( + // FuelError.CODES.INVALID_REQUEST, + // errors.map((err) => err.message).join('\n\n') + // ) + // ) + // ); + // }); + // }) + // .then( + // (x) => x, + // (reason) => abortController.abort() + // ) + // ); + } + + return gqlClient.request(query, vars); }); } diff --git a/packages/providers/src/subscriber.ts b/packages/providers/src/subscriber.ts deleted file mode 100644 index 9ec7730c0fc..00000000000 --- a/packages/providers/src/subscriber.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { FuelError } from '@fuel-ts/errors'; -import type { DocumentNode } from 'graphql'; -import { print } from 'graphql'; - -type SubscriberOptions = { - url: string; - query: DocumentNode; - variables?: Record; - fetchFn: typeof fetch; - abortController?: AbortController; -}; -export class Subscriber { - private request: Promise; - private reader?: ReadableStreamDefaultReader; - private abortController: AbortController; - constructor({ - url, - query, - variables, - fetchFn, - abortController = new AbortController(), - }: SubscriberOptions) { - const queryString = print(query); - const requestBody = { - query: queryString, - variables, - }; - this.abortController = abortController; - this.request = fetchFn(`${url}-sub`, { - method: 'POST', - signal: this.abortController.signal, - body: JSON.stringify(requestBody), - keepalive: true, - headers: { - 'Content-Type': 'application/json', - Accept: 'text/event-stream', - }, - }); - } - async sub() { - const generator = this.subscribe(); - generator.return = () => {}; - } - - async *subscribe() { - if (this.reader === undefined) { - const response = await this.request; - this.reader = response.body!.getReader(); - } - - while (true) { - const { value, done } = await this.reader.read(); - const text = new TextDecoder().decode(value); - if (text.startsWith('data:')) { - const { data, errors } = JSON.parse(text.split('data:')[1]); - if (Array.isArray(errors)) { - await this.reader.cancel(); - // @ts-expect-error to dispose - this.request = null; - this.reader = undefined; - - this.abortController.abort(); - throw new FuelError( - FuelError.CODES.INVALID_REQUEST, - errors.map((x) => x.message).join('\n\n') - ); - } - yield data; - if (done) break; - } - } - } -} diff --git a/packages/providers/src/transaction-response/transaction-response.ts b/packages/providers/src/transaction-response/transaction-response.ts index 3f4587ed4aa..a105a8fd691 100644 --- a/packages/providers/src/transaction-response/transaction-response.ts +++ b/packages/providers/src/transaction-response/transaction-response.ts @@ -72,9 +72,6 @@ export type TransactionResultReceipt = | TransactionResultMintReceipt | TransactionResultBurnReceipt; -const STATUS_POLLING_INTERVAL_MAX_MS = 5000; -const STATUS_POLLING_INTERVAL_MIN_MS = 1000; - /** @hidden */ export type TransactionResult = TransactionSummary & { gqlTransaction: GqlTransaction; @@ -90,10 +87,6 @@ export class TransactionResponse { provider: Provider; /** Gas used on the transaction */ gasUsed: BN = bn(0); - /** Number of attempts made to fetch the transaction */ - fetchAttempts: number = 0; - /** Number of attempts made to retrieve a processed transaction. */ - resultAttempts: number = 0; /** The graphql Transaction with receipts object. */ gqlTransaction?: GqlTransaction; @@ -215,12 +208,8 @@ export class TransactionResponse { })) { if (res.statusChange.type !== 'SubmittedStatus') break; } - await this.fetch(); - // if (this.gqlTransaction?.status?.type === 'SubmittedStatus') { - // await this.sleepBasedOnAttempts(++this.resultAttempts); - // return this.waitForResult(contractsAbiMap); - // } + await this.fetch(); const transactionSummary = await this.getTransactionSummary(contractsAbiMap); @@ -251,18 +240,4 @@ export class TransactionResponse { return result; } - - /** - * Introduces a delay based on the number of previous attempts made. - * - * @param attempts - The number of attempts. - */ - private async sleepBasedOnAttempts(attempts: number): Promise { - // TODO: Consider adding `maxTimeout` or `maxAttempts` parameter. - // The aim is to avoid perpetual execution; when the limit - // is reached, we can throw accordingly. - await sleep( - Math.min(STATUS_POLLING_INTERVAL_MIN_MS * attempts, STATUS_POLLING_INTERVAL_MAX_MS) - ); - } } From 8f66eff069617a635ee0c48ff00bbbd2c8e8b2e0 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 14:26:24 +0200 Subject: [PATCH 05/51] test: moved tests --- .../src/transaction-response.test.ts | 26 ------------------- packages/providers/test/provider.test.ts | 24 +++++++++++++++++ 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/packages/fuel-gauge/src/transaction-response.test.ts b/packages/fuel-gauge/src/transaction-response.test.ts index a6e8477196e..dabc4c5d1ee 100644 --- a/packages/fuel-gauge/src/transaction-response.test.ts +++ b/packages/fuel-gauge/src/transaction-response.test.ts @@ -1,5 +1,3 @@ -import { FuelError } from '@fuel-ts/errors'; -import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils'; import { generateTestWallet, launchNode } from '@fuel-ts/wallet/test-utils'; import type { BN } from 'fuels'; import { @@ -114,28 +112,4 @@ describe('TransactionSummary', () => { cleanup(); }, 25000); - - it('ensure that an invalid request throws and does not hold test runner (closes all handles)', async () => { - const { cleanup, ip, port } = await launchNode({}); - const nodeProvider = await Provider.create(`http://${ip}:${port}/graphql`); - - const response = new TransactionResponse('asdsadflk3jeh', nodeProvider); - - await expectToThrowFuelError(() => response.waitForResult(), { - code: FuelError.CODES.INVALID_REQUEST, - }); - - await expectToThrowFuelError( - async () => { - for await (const value of nodeProvider.operations.statusChange({ - transactionId: 'asdfkljer', - })) { - console.log(value); - } - }, - - { code: FuelError.CODES.INVALID_REQUEST } - ); - cleanup(); - }); }); diff --git a/packages/providers/test/provider.test.ts b/packages/providers/test/provider.test.ts index 5d85a946b95..e146ecc9469 100644 --- a/packages/providers/test/provider.test.ts +++ b/packages/providers/test/provider.test.ts @@ -18,6 +18,7 @@ import type { MessageTransactionRequestInput, } from '../src/transaction-request'; import { ScriptTransactionRequest } from '../src/transaction-request'; +import { TransactionResponse } from '../src/transaction-response'; import { fromTai64ToUnix, fromUnixToTai64 } from '../src/utils'; import { messageProofResponse, messageStatusResponse } from './fixtures'; @@ -888,4 +889,27 @@ describe('Provider', () => { expect(txCostSpy).toHaveBeenCalled(); expect(estimateTxSpy).toHaveBeenCalled(); }); + + it('ensure that an invalid request throws and does not hold the test runner (closes all handles)', async () => { + const provider = await Provider.create(FUEL_NETWORK_URL); + + await expectToThrowFuelError( + async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const value of provider.operations.statusChange({ + transactionId: 'asdfkljer', + })) { + // + } + }, + + { code: FuelError.CODES.INVALID_REQUEST } + ); + + const response = new TransactionResponse('asdsadflk3jeh', provider); + + await expectToThrowFuelError(() => response.waitForResult(), { + code: FuelError.CODES.INVALID_REQUEST, + }); + }); }); From abb2085f2636d00c2fef04ecb0ef5dc5ab934258 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 14:30:14 +0200 Subject: [PATCH 06/51] refactor: to use async/await a bit more --- .../providers/src/fuel-graphql-subscriber.ts | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/packages/providers/src/fuel-graphql-subscriber.ts b/packages/providers/src/fuel-graphql-subscriber.ts index cb88625657c..c94cf4e0103 100644 --- a/packages/providers/src/fuel-graphql-subscriber.ts +++ b/packages/providers/src/fuel-graphql-subscriber.ts @@ -16,7 +16,7 @@ export async function* fuelGraphQLSubscriber({ query, fetchFn, }: FuelGraphQLSubscriberOptions) { - const streamReader = await fetchFn(`${url}-sub`, { + const response = await fetchFn(`${url}-sub`, { method: 'POST', keepalive: true, body: JSON.stringify({ @@ -27,35 +27,33 @@ export async function* fuelGraphQLSubscriber({ 'Content-Type': 'application/json', Accept: 'text/event-stream', }, - }).then((x) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const reader = x.body!.pipeThrough(new TextDecoderStream()).getReader(); + }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader(); - return new ReadableStream({ - start(controller) { - reader.read().then(function push(result) { - const { done, value } = result; - if (done) { - controller.close(); - return; - } - if (value.startsWith('data:')) { - const { data, errors } = JSON.parse(value.split('data:')[1]); - if (Array.isArray(errors)) { - controller.enqueue( - new FuelError( - FuelError.CODES.INVALID_REQUEST, - errors.map((err) => err.message).join('\n\n') - ) - ); - } else controller.enqueue(data); - } + const streamReader = new ReadableStream({ + start(controller) { + reader.read().then(function push({ value, done }) { + if (done) { + controller.close(); + return; + } + if (value.startsWith('data:')) { + const { data, errors } = JSON.parse(value.split('data:')[1]); + if (Array.isArray(errors)) { + controller.enqueue( + new FuelError( + FuelError.CODES.INVALID_REQUEST, + errors.map((err) => err.message).join('\n\n') + ) + ); + } else controller.enqueue(data); + } - reader.read().then(push); - }); - }, - }).getReader(); - }); + reader.read().then(push); + }); + }, + }).getReader(); for (;;) { const { value, done } = await streamReader.read(); From b9517c6ff6fc2966f4c7118fa94ba0017163cec9 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 15:37:07 +0200 Subject: [PATCH 07/51] refactor: some more --- packages/providers/src/fuel-graphql-subscriber.ts | 10 +++++----- packages/providers/test/provider.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/providers/src/fuel-graphql-subscriber.ts b/packages/providers/src/fuel-graphql-subscriber.ts index c94cf4e0103..6c4b747651f 100644 --- a/packages/providers/src/fuel-graphql-subscriber.ts +++ b/packages/providers/src/fuel-graphql-subscriber.ts @@ -32,12 +32,14 @@ export async function* fuelGraphQLSubscriber({ const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader(); const streamReader = new ReadableStream({ - start(controller) { - reader.read().then(function push({ value, done }) { + async start(controller) { + for (;;) { + const { value, done } = await reader.read(); if (done) { controller.close(); return; } + // the fuel node sends keep-alive messages that should be ignored if (value.startsWith('data:')) { const { data, errors } = JSON.parse(value.split('data:')[1]); if (Array.isArray(errors)) { @@ -49,9 +51,7 @@ export async function* fuelGraphQLSubscriber({ ); } else controller.enqueue(data); } - - reader.read().then(push); - }); + } }, }).getReader(); diff --git a/packages/providers/test/provider.test.ts b/packages/providers/test/provider.test.ts index e146ecc9469..a7a7dbaa03b 100644 --- a/packages/providers/test/provider.test.ts +++ b/packages/providers/test/provider.test.ts @@ -897,7 +897,7 @@ describe('Provider', () => { async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars for await (const value of provider.operations.statusChange({ - transactionId: 'asdfkljer', + transactionId: 'invalid transaction id', })) { // } @@ -906,7 +906,7 @@ describe('Provider', () => { { code: FuelError.CODES.INVALID_REQUEST } ); - const response = new TransactionResponse('asdsadflk3jeh', provider); + const response = new TransactionResponse('invalid transaction id', provider); await expectToThrowFuelError(() => response.waitForResult(), { code: FuelError.CODES.INVALID_REQUEST, From 3626eebfa29d556731f8d652d0b45e5e7a27b149 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 15:50:58 +0200 Subject: [PATCH 08/51] chore: updated nodejs version --- .github/actions/ci-setup/action.yaml | 2 +- .nvmrc | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/ci-setup/action.yaml b/.github/actions/ci-setup/action.yaml index 503ae5c9af0..b25763b7085 100644 --- a/.github/actions/ci-setup/action.yaml +++ b/.github/actions/ci-setup/action.yaml @@ -2,7 +2,7 @@ name: "CI setup" inputs: node-version: description: "Node version" - default: 18.14.1 + default: 18.18.2 pnpm-version: description: "PNPM version" default: 8.9.0 diff --git a/.nvmrc b/.nvmrc index 617bcf916bf..87ec8842b15 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.14.1 +18.18.2 diff --git a/package.json b/package.json index d0fda33893d..59354ad4821 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "Fuel Labs (https://fuel.network/)", "private": true, "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", From 305baeee29e01c8e8ef7b34634fc2c4c0a33ce23 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 16:15:38 +0200 Subject: [PATCH 09/51] feat: ability to set timeout --- packages/providers/src/provider.ts | 92 ++++++++++-------------- packages/providers/test/provider.test.ts | 79 ++++++++++++++++---- 2 files changed, 101 insertions(+), 70 deletions(-) diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index 803bea017fe..5fd0368584f 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -200,7 +200,12 @@ export type FetchRequestOptions = { * Provider initialization options */ export type ProviderOptions = { - fetch?: (url: string, options: FetchRequestOptions) => Promise; + fetch?: ( + url: string, + options: FetchRequestOptions, + providerOptions: ProviderOptions + ) => Promise; + timeout?: number; cacheUtxo?: number; }; @@ -236,6 +241,23 @@ export default class Provider { private static chainInfoCache: ChainInfoCache = {}; private static nodeInfoCache: NodeInfoCache = {}; + options: ProviderOptions = { + timeout: undefined, + cacheUtxo: undefined, + fetch: undefined, + }; + + private static getFetchFn(options: ProviderOptions) { + return options.fetch !== undefined + ? options.fetch + : (url: string, request: FetchRequestOptions) => + fetch(url, { + ...request, + signal: + options.timeout !== undefined ? AbortSignal.timeout(options.timeout) : undefined, + }); + } + /** * Constructor to initialize a Provider. * @@ -247,9 +269,12 @@ export default class Provider { protected constructor( /** GraphQL endpoint of the Fuel node */ public url: string, - public options: ProviderOptions = {} + options: ProviderOptions = {} ) { - this.operations = this.createOperations(url, options); + this.options = { ...this.options, ...options }; + this.url = url; + + this.operations = this.createOperations(); this.cache = options.cacheUtxo ? new MemoryCache(options.cacheUtxo) : undefined; } @@ -313,7 +338,7 @@ export default class Provider { */ async connect(url: string) { this.url = url; - this.operations = this.createOperations(url); + this.operations = this.createOperations(); await this.fetchChainAndNodeInfo(); } @@ -349,15 +374,14 @@ export default class Provider { /** * Create GraphQL client and set operations. * - * @param url - The URL of the Fuel node - * @param options - Additional options for the provider * @returns The operation SDK object */ - private createOperations(url: string, options: ProviderOptions = {}) { - this.url = url; - - const fetchFn = (options.fetch as typeof fetch) ?? fetch; - const gqlClient = new GraphQLClient(url, { fetch: fetchFn }); + private createOperations() { + const fetchFn = Provider.getFetchFn(this.options); + const gqlClient = new GraphQLClient(this.url, { + fetch: (url: string, requestInit: FetchRequestOptions) => + fetchFn(url, requestInit, this.options), + }); // @ts-expect-error This is due to this function being generic and us using multiple libraries. Its type is specified when calling a specific operation via provider.operations.xyz. return getOperationsSdk((query, vars) => { @@ -369,52 +393,10 @@ export default class Provider { return fuelGraphQLSubscriber({ url: this.url, query, - fetchFn, + fetchFn: (url, requestInit) => + fetchFn(url as string, requestInit as FetchRequestOptions, this.options), variables: vars as Record, }); - // return subscriber((abortController) => - // fetchFn(`${url}-sub`, { - // method: 'POST', - // signal: abortController.signal, - // body: JSON.stringify(requestBody), - // headers: { - // 'Content-Type': 'application/json', - // Accept: 'text/event-stream', - // }, - // }) - // .then((x) => { - // const reader = x.body!.getReader(); - - // let text = ''; - // // @ts-expect-error asd - // return reader.read().then(function process(result) { - // text += new TextDecoder().decode(result.value); - // if (!result.done) return reader.read().then(process); - // if (!text.startsWith('data:')) { - // text = ''; - // return reader.read().then(process); - // } - - // const { data, errors } = JSON.parse(text.split('data:')[1]); - // if (!Array.isArray(errors)) return data; - - // return reader - // .cancel() - // .then(() => - // Promise.reject( - // new FuelError( - // FuelError.CODES.INVALID_REQUEST, - // errors.map((err) => err.message).join('\n\n') - // ) - // ) - // ); - // }); - // }) - // .then( - // (x) => x, - // (reason) => abortController.abort() - // ) - // ); } return gqlClient.request(query, vars); diff --git a/packages/providers/test/provider.test.ts b/packages/providers/test/provider.test.ts index a7a7dbaa03b..97750046ee4 100644 --- a/packages/providers/test/provider.test.ts +++ b/packages/providers/test/provider.test.ts @@ -12,6 +12,7 @@ import { getBytesCopy, hexlify } from 'ethers'; import type { BytesLike } from 'ethers'; import * as GraphQL from 'graphql-request'; +import type { FetchRequestOptions } from '../src/provider'; import Provider from '../src/provider'; import type { CoinTransactionRequestInput, @@ -19,7 +20,7 @@ import type { } from '../src/transaction-request'; import { ScriptTransactionRequest } from '../src/transaction-request'; import { TransactionResponse } from '../src/transaction-response'; -import { fromTai64ToUnix, fromUnixToTai64 } from '../src/utils'; +import { fromTai64ToUnix, fromUnixToTai64, sleep } from '../src/utils'; import { messageProofResponse, messageStatusResponse } from './fixtures'; import { MOCK_CHAIN } from './fixtures/chain'; @@ -206,22 +207,15 @@ describe('Provider', () => { it('can change the provider url of the current instance', async () => { const providerUrl1 = FUEL_NETWORK_URL; - const providerUrl2 = 'http://127.0.0.1:8080/graphql'; + const providerUrl2 = 'https://beta-4.fuel.network/graphql'; - const provider = await Provider.create(providerUrl1); + const provider = await Provider.create(providerUrl1, { + fetch: (url: string, options: FetchRequestOptions) => + getCustomFetch('getVersion', { nodeInfo: { nodeVersion: url } })(url, options), + }); expect(provider.url).toBe(providerUrl1); - - const spyGraphQLClient = jest.spyOn(GraphQL, 'GraphQLClient').mockImplementation( - () => - ({ - request: () => - Promise.resolve({ - chain: MOCK_CHAIN, - nodeInfo: MOCK_NODE_INFO, - }), - } as unknown as GraphQL.GraphQLClient) - ); + expect(await provider.getVersion()).toEqual(providerUrl1); const spyFetchChainAndNodeInfo = jest.spyOn(Provider.prototype, 'fetchChainAndNodeInfo'); const spyFetchChain = jest.spyOn(Provider.prototype, 'fetchChain'); @@ -229,7 +223,8 @@ describe('Provider', () => { await provider.connect(providerUrl2); expect(provider.url).toBe(providerUrl2); - expect(spyGraphQLClient).toBeCalledWith(providerUrl2, undefined); + + expect(await provider.getVersion()).toEqual(providerUrl2); expect(spyFetchChainAndNodeInfo).toHaveBeenCalledTimes(1); expect(spyFetchChain).toHaveBeenCalledTimes(1); @@ -912,4 +907,58 @@ describe('Provider', () => { code: FuelError.CODES.INVALID_REQUEST, }); }); + + it('default timeout is undefined', async () => { + const provider = await Provider.create(FUEL_NETWORK_URL); + expect(provider.options.timeout).toBeUndefined(); + }); + + it('throws TimeoutError on timeout when calling an operation', async () => { + const timeout = 500; + const provider = await Provider.create(FUEL_NETWORK_URL, { timeout }); + jest + .spyOn(global, 'fetch') + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TS is throwing error in test, but not in IDE + .mockImplementationOnce((input: RequestInfo | URL, init: RequestInit | undefined) => + sleep(timeout).then(() => fetch(input, init)) + ); + + const { error } = await safeExec(async () => { + await provider.getBlocks({}); + }); + + expect(error).toMatchObject({ + code: 23, + name: 'TimeoutError', + message: 'The operation was aborted due to timeout', + }); + }); + + // skipped because graphql-sse creates their own AbortController which controls timeouts. https://github.com/FuelLabs/fuels-ts/issues/1293 + it.skip('throws TimeoutError on timeout when calling a subscription', async () => { + const timeout = 500; + const provider = await Provider.create(FUEL_NETWORK_URL, { timeout }); + + jest + .spyOn(global, 'fetch') + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TS is throwing error in test, but not in IDE + .mockImplementationOnce((input: RequestInfo | URL, init: RequestInit | undefined) => + sleep(timeout).then(() => fetch(input, init)) + ); + + const { error } = await safeExec(async () => { + for await (const iterator of provider.operations.statusChange({ + transactionId: 'doesnt matter, will be aborted', + })) { + // shouldn't be reached + } + }); + expect(error).toMatchObject({ + code: 23, + name: 'TimeoutError', + message: 'The operation was aborted due to timeout', + }); + }); }); From 30357b66b7824bc7c9728241f43e5715f9463334 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 16:22:18 +0200 Subject: [PATCH 10/51] fix explanation --- packages/providers/src/provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index 5fd0368584f..44bfdd499a6 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -383,7 +383,7 @@ export default class Provider { fetchFn(url, requestInit, this.options), }); - // @ts-expect-error This is due to this function being generic and us using multiple libraries. Its type is specified when calling a specific operation via provider.operations.xyz. + // @ts-expect-error This is due to this function being generic. Its type is specified when calling a specific operation via provider.operations.xyz. return getOperationsSdk((query, vars) => { const isSubscription = (query.definitions.find((x) => x.kind === 'OperationDefinition') as { operation: string }) From dda0faeef9cb1e18e78d3ac0a6d70e262cc8595b Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 16:23:58 +0200 Subject: [PATCH 11/51] refactor: variable use --- .../src/transaction-response/transaction-response.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/providers/src/transaction-response/transaction-response.ts b/packages/providers/src/transaction-response/transaction-response.ts index b2600e31af1..a3bd3c850d7 100644 --- a/packages/providers/src/transaction-response/transaction-response.ts +++ b/packages/providers/src/transaction-response/transaction-response.ts @@ -29,7 +29,6 @@ import type { GqlTransaction, AbiMap, } from '../transaction-summary/types'; -import { sleep } from '../utils'; /** @hidden */ export type TransactionResultCallReceipt = ReceiptCall; @@ -126,12 +125,10 @@ export class TransactionResponse { }); if (!response.transaction) { - for await (const res of this.provider.operations.statusChange({ + for await (const { statusChange } of this.provider.operations.statusChange({ transactionId: this.id, })) { - if (res.statusChange) { - break; - } + if (statusChange) break; } return this.fetch(); @@ -203,10 +200,10 @@ export class TransactionResponse { async waitForResult( contractsAbiMap?: AbiMap ): Promise> { - for await (const res of this.provider.operations.statusChange({ + for await (const { statusChange } of this.provider.operations.statusChange({ transactionId: this.id, })) { - if (res.statusChange.type !== 'SubmittedStatus') break; + if (statusChange.type !== 'SubmittedStatus') break; } await this.fetch(); From 523905895201931dfffbd967ac1e05b7b2e28c62 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 16:25:31 +0200 Subject: [PATCH 12/51] test: better naming --- packages/providers/test/provider.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/providers/test/provider.test.ts b/packages/providers/test/provider.test.ts index 97750046ee4..20633e328f0 100644 --- a/packages/providers/test/provider.test.ts +++ b/packages/providers/test/provider.test.ts @@ -885,7 +885,7 @@ describe('Provider', () => { expect(estimateTxSpy).toHaveBeenCalled(); }); - it('ensure that an invalid request throws and does not hold the test runner (closes all handles)', async () => { + it('An invalid subscription request throws and does not hold the test runner (closes all handles)', async () => { const provider = await Provider.create(FUEL_NETWORK_URL); await expectToThrowFuelError( From 06d58cd3aaa1851e919710707e7e0a7364e24094 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 16:25:59 +0200 Subject: [PATCH 13/51] test: enable test --- packages/providers/test/provider.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/providers/test/provider.test.ts b/packages/providers/test/provider.test.ts index 20633e328f0..ceb4c3ff2f2 100644 --- a/packages/providers/test/provider.test.ts +++ b/packages/providers/test/provider.test.ts @@ -935,8 +935,7 @@ describe('Provider', () => { }); }); - // skipped because graphql-sse creates their own AbortController which controls timeouts. https://github.com/FuelLabs/fuels-ts/issues/1293 - it.skip('throws TimeoutError on timeout when calling a subscription', async () => { + it('throws TimeoutError on timeout when calling a subscription', async () => { const timeout = 500; const provider = await Provider.create(FUEL_NETWORK_URL, { timeout }); @@ -949,6 +948,7 @@ describe('Provider', () => { ); const { error } = await safeExec(async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars for await (const iterator of provider.operations.statusChange({ transactionId: 'doesnt matter, will be aborted', })) { From 4420a43bde5060cd6ad8455c468d621af3fb8a0b Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 16:29:08 +0200 Subject: [PATCH 14/51] removed unnecessary keepalive (set by server) --- packages/providers/src/fuel-graphql-subscriber.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/providers/src/fuel-graphql-subscriber.ts b/packages/providers/src/fuel-graphql-subscriber.ts index 6c4b747651f..25259b4e157 100644 --- a/packages/providers/src/fuel-graphql-subscriber.ts +++ b/packages/providers/src/fuel-graphql-subscriber.ts @@ -18,7 +18,6 @@ export async function* fuelGraphQLSubscriber({ }: FuelGraphQLSubscriberOptions) { const response = await fetchFn(`${url}-sub`, { method: 'POST', - keepalive: true, body: JSON.stringify({ query: print(query), variables, From 77cfe161dbefe0339ef606b6e09e200276d69ffe Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 16:38:22 +0200 Subject: [PATCH 15/51] chore: changeset --- .changeset/serious-laws-judge.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/serious-laws-judge.md diff --git a/.changeset/serious-laws-judge.md b/.changeset/serious-laws-judge.md new file mode 100644 index 00000000000..23babd84b11 --- /dev/null +++ b/.changeset/serious-laws-judge.md @@ -0,0 +1,6 @@ +--- +"@fuel-ts/providers": minor +"@fuel-ts/errors": patch +--- + +Implemented GraphQL subscriptions From 4989346535b027bff55d8dc625750b7ee285a968 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 16:43:39 +0200 Subject: [PATCH 16/51] revert settings.json --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3bab6ff3d03..9265bce8080 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "editor.defaultFormatter": "esbenp.prettier-vscode", + "prettier.prettierPath": "./node_modules/prettier", "prettier.configPath": ".prettierrc", "editor.formatOnSave": true, "editor.codeActionsOnSave": { From 75eedea009a6d43b153f6a77fcb6f23b5bb2ef24 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 17:26:05 +0200 Subject: [PATCH 17/51] refactor: use pipeThrough with custom TransformStream --- .../providers/src/fuel-graphql-subscriber.ts | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/packages/providers/src/fuel-graphql-subscriber.ts b/packages/providers/src/fuel-graphql-subscriber.ts index 25259b4e157..859d74f6f4a 100644 --- a/packages/providers/src/fuel-graphql-subscriber.ts +++ b/packages/providers/src/fuel-graphql-subscriber.ts @@ -10,6 +10,39 @@ type FuelGraphQLSubscriberOptions = { abortController?: AbortController; }; +class FuelSubscriptionStream implements TransformStream { + readable: ReadableStream; + writable: WritableStream; + private readableStreamController!: ReadableStreamController; + private static textDecoder = new TextDecoder(); + + constructor() { + this.readable = new ReadableStream({ + start: (controller) => { + this.readableStreamController = controller; + }, + }); + + this.writable = new WritableStream({ + write: (bytes) => { + const text = FuelSubscriptionStream.textDecoder.decode(bytes); + // the fuel node sends keep-alive messages that should be ignored + if (text.startsWith('data:')) { + const { data, errors } = JSON.parse(text.split('data:')[1]); + if (Array.isArray(errors)) { + this.readableStreamController.enqueue( + new FuelError( + FuelError.CODES.INVALID_REQUEST, + errors.map((err) => err.message).join('\n\n') + ) + ); + } else this.readableStreamController.enqueue(data); + } + }, + }); + } +} + export async function* fuelGraphQLSubscriber({ url, variables, @@ -27,32 +60,9 @@ export async function* fuelGraphQLSubscriber({ Accept: 'text/event-stream', }, }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader(); - const streamReader = new ReadableStream({ - async start(controller) { - for (;;) { - const { value, done } = await reader.read(); - if (done) { - controller.close(); - return; - } - // the fuel node sends keep-alive messages that should be ignored - if (value.startsWith('data:')) { - const { data, errors } = JSON.parse(value.split('data:')[1]); - if (Array.isArray(errors)) { - controller.enqueue( - new FuelError( - FuelError.CODES.INVALID_REQUEST, - errors.map((err) => err.message).join('\n\n') - ) - ); - } else controller.enqueue(data); - } - } - }, - }).getReader(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const streamReader = response.body!.pipeThrough(new FuelSubscriptionStream()).getReader(); for (;;) { const { value, done } = await streamReader.read(); From 1cf4339db81df3ff90eba74f149f2b24c75367f9 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 17:26:27 +0200 Subject: [PATCH 18/51] refactor: types --- packages/providers/src/fuel-graphql-subscriber.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/providers/src/fuel-graphql-subscriber.ts b/packages/providers/src/fuel-graphql-subscriber.ts index 859d74f6f4a..3f512ae5ab8 100644 --- a/packages/providers/src/fuel-graphql-subscriber.ts +++ b/packages/providers/src/fuel-graphql-subscriber.ts @@ -11,9 +11,9 @@ type FuelGraphQLSubscriberOptions = { }; class FuelSubscriptionStream implements TransformStream { - readable: ReadableStream; + readable: ReadableStream>; writable: WritableStream; - private readableStreamController!: ReadableStreamController; + private readableStreamController!: ReadableStreamController>; private static textDecoder = new TextDecoder(); constructor() { From 268088aa986a79acb1a41c90146d81dec5e94bd8 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 17:27:03 +0200 Subject: [PATCH 19/51] refactor: naming --- packages/providers/src/fuel-graphql-subscriber.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/providers/src/fuel-graphql-subscriber.ts b/packages/providers/src/fuel-graphql-subscriber.ts index 3f512ae5ab8..d6d84a33370 100644 --- a/packages/providers/src/fuel-graphql-subscriber.ts +++ b/packages/providers/src/fuel-graphql-subscriber.ts @@ -62,10 +62,12 @@ export async function* fuelGraphQLSubscriber({ }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const streamReader = response.body!.pipeThrough(new FuelSubscriptionStream()).getReader(); + const subscriptionsStreamReader = response + .body!.pipeThrough(new FuelSubscriptionStream()) + .getReader(); for (;;) { - const { value, done } = await streamReader.read(); + const { value, done } = await subscriptionsStreamReader.read(); if (value instanceof FuelError) throw value; yield value; if (done) break; From 620de31724d06f4c9c45ae2da80082ddce08670a Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 18:15:36 +0200 Subject: [PATCH 20/51] refactor: ternary operator ftw --- packages/providers/src/provider.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index 44bfdd499a6..66edbeb4fb3 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -389,17 +389,15 @@ export default class Provider { (query.definitions.find((x) => x.kind === 'OperationDefinition') as { operation: string }) ?.operation === 'subscription'; - if (isSubscription) { - return fuelGraphQLSubscriber({ - url: this.url, - query, - fetchFn: (url, requestInit) => - fetchFn(url as string, requestInit as FetchRequestOptions, this.options), - variables: vars as Record, - }); - } - - return gqlClient.request(query, vars); + return isSubscription + ? fuelGraphQLSubscriber({ + url: this.url, + query, + fetchFn: (url, requestInit) => + fetchFn(url as string, requestInit as FetchRequestOptions, this.options), + variables: vars as Record, + }) + : gqlClient.request(query, vars); }); } From 8d46b15297863207432d09317b298890400b21a3 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 18:17:31 +0200 Subject: [PATCH 21/51] refactor: omit fetch from options passed to custom fetch --- packages/providers/src/provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index 66edbeb4fb3..6db54fc1b60 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -203,7 +203,7 @@ export type ProviderOptions = { fetch?: ( url: string, options: FetchRequestOptions, - providerOptions: ProviderOptions + providerOptions: Omit ) => Promise; timeout?: number; cacheUtxo?: number; From 6368f210bdf0b1d2711761ad7c01d046e25c152d Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 18:18:34 +0200 Subject: [PATCH 22/51] test: explanations --- packages/providers/test/provider.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/providers/test/provider.test.ts b/packages/providers/test/provider.test.ts index ceb4c3ff2f2..1fe7b716b76 100644 --- a/packages/providers/test/provider.test.ts +++ b/packages/providers/test/provider.test.ts @@ -885,7 +885,7 @@ describe('Provider', () => { expect(estimateTxSpy).toHaveBeenCalled(); }); - it('An invalid subscription request throws and does not hold the test runner (closes all handles)', async () => { + it('An invalid subscription request throws a FuelError and does not hold the test runner (closes all handles)', async () => { const provider = await Provider.create(FUEL_NETWORK_URL); await expectToThrowFuelError( @@ -919,7 +919,7 @@ describe('Provider', () => { jest .spyOn(global, 'fetch') // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore TS is throwing error in test, but not in IDE + // @ts-ignore TS is throwing error when test is run, but not in IDE .mockImplementationOnce((input: RequestInfo | URL, init: RequestInit | undefined) => sleep(timeout).then(() => fetch(input, init)) ); @@ -942,7 +942,7 @@ describe('Provider', () => { jest .spyOn(global, 'fetch') // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore TS is throwing error in test, but not in IDE + // @ts-ignore TS is throwing error when test is run, but not in IDE .mockImplementationOnce((input: RequestInfo | URL, init: RequestInit | undefined) => sleep(timeout).then(() => fetch(input, init)) ); From 40e0dbabd1597940414830e703e9bd71deeec69a Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 25 Oct 2023 18:21:02 +0200 Subject: [PATCH 23/51] refactor: var name --- packages/providers/src/fuel-graphql-subscriber.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/providers/src/fuel-graphql-subscriber.ts b/packages/providers/src/fuel-graphql-subscriber.ts index d6d84a33370..e88bb33025e 100644 --- a/packages/providers/src/fuel-graphql-subscriber.ts +++ b/packages/providers/src/fuel-graphql-subscriber.ts @@ -62,12 +62,12 @@ export async function* fuelGraphQLSubscriber({ }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const subscriptionsStreamReader = response + const subscriptionStreamReader = response .body!.pipeThrough(new FuelSubscriptionStream()) .getReader(); for (;;) { - const { value, done } = await subscriptionsStreamReader.read(); + const { value, done } = await subscriptionStreamReader.read(); if (value instanceof FuelError) throw value; yield value; if (done) break; From 9c086f63f24350f4d2d6dbf456a650ec4b86ec5f Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 3 Nov 2023 15:40:52 +0100 Subject: [PATCH 24/51] chore: update all packages to use node v18.18.2 --- packages/abi-coder/package.json | 2 +- packages/abi-typegen/package.json | 2 +- packages/address/package.json | 2 +- packages/contract/package.json | 2 +- packages/crypto/package.json | 2 +- packages/errors/package.json | 2 +- packages/fuels/package.json | 2 +- packages/hasher/package.json | 2 +- packages/hdwallet/package.json | 2 +- packages/interfaces/package.json | 2 +- packages/math/package.json | 2 +- packages/merkle/package.json | 2 +- packages/mnemonic/package.json | 2 +- packages/predicate/package.json | 2 +- packages/program/package.json | 2 +- packages/providers/package.json | 2 +- packages/script/package.json | 2 +- packages/signer/package.json | 2 +- packages/transactions/package.json | 2 +- packages/utils/package.json | 2 +- packages/versions/package.json | 2 +- packages/wallet-manager/package.json | 2 +- packages/wallet/package.json | 2 +- packages/wordlists/package.json | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/abi-coder/package.json b/packages/abi-coder/package.json index dd925254005..43739aa508a 100644 --- a/packages/abi-coder/package.json +++ b/packages/abi-coder/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/abi-typegen/package.json b/packages/abi-typegen/package.json index 47b180faadc..882eda98b69 100644 --- a/packages/abi-typegen/package.json +++ b/packages/abi-typegen/package.json @@ -10,7 +10,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/address/package.json b/packages/address/package.json index c98d225e8c0..6038e572031 100644 --- a/packages/address/package.json +++ b/packages/address/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/contract/package.json b/packages/contract/package.json index fb245c809ab..ad8957acb0f 100644 --- a/packages/contract/package.json +++ b/packages/contract/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/crypto/package.json b/packages/crypto/package.json index aca91a955c2..2594b185f36 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/errors/package.json b/packages/errors/package.json index 19f53110af0..c058f6b97c9 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/fuels/package.json b/packages/fuels/package.json index 06bbbcb5aa3..7e6032a22bf 100644 --- a/packages/fuels/package.json +++ b/packages/fuels/package.json @@ -10,7 +10,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/hasher/package.json b/packages/hasher/package.json index 8bfdf9f3d2b..248f6907922 100644 --- a/packages/hasher/package.json +++ b/packages/hasher/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/hdwallet/package.json b/packages/hdwallet/package.json index d27e04c0ad2..fe3a192e701 100644 --- a/packages/hdwallet/package.json +++ b/packages/hdwallet/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/interfaces/package.json b/packages/interfaces/package.json index 6e68c5e646a..b04b47c6e90 100644 --- a/packages/interfaces/package.json +++ b/packages/interfaces/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/math/package.json b/packages/math/package.json index 9ab2832934e..f0d8a289d12 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/merkle/package.json b/packages/merkle/package.json index 4b7d09d25de..a230a4f8e5c 100644 --- a/packages/merkle/package.json +++ b/packages/merkle/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/mnemonic/package.json b/packages/mnemonic/package.json index 9392bf28c5f..2e5618741fa 100644 --- a/packages/mnemonic/package.json +++ b/packages/mnemonic/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/predicate/package.json b/packages/predicate/package.json index fada3396c60..4e144df44f3 100644 --- a/packages/predicate/package.json +++ b/packages/predicate/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/program/package.json b/packages/program/package.json index d664983b4f0..d277fc5ff38 100644 --- a/packages/program/package.json +++ b/packages/program/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/providers/package.json b/packages/providers/package.json index 9fde06effd1..3fcc3ad1ddb 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/script/package.json b/packages/script/package.json index f5e2afef95c..c807f55f853 100644 --- a/packages/script/package.json +++ b/packages/script/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/signer/package.json b/packages/signer/package.json index cc0a7315f61..83618843475 100644 --- a/packages/signer/package.json +++ b/packages/signer/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/transactions/package.json b/packages/transactions/package.json index fdc0c0063ce..e758c064a3f 100644 --- a/packages/transactions/package.json +++ b/packages/transactions/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/utils/package.json b/packages/utils/package.json index d479115455c..fb5a12db758 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/versions/package.json b/packages/versions/package.json index f94b69ab6cd..8a80c0be8b0 100644 --- a/packages/versions/package.json +++ b/packages/versions/package.json @@ -10,7 +10,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/wallet-manager/package.json b/packages/wallet-manager/package.json index 9ed91d13245..0c30ccf8dee 100644 --- a/packages/wallet-manager/package.json +++ b/packages/wallet-manager/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 1ea081e49a3..2c669437093 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", diff --git a/packages/wordlists/package.json b/packages/wordlists/package.json index 82e63decbe6..8dd48e39769 100644 --- a/packages/wordlists/package.json +++ b/packages/wordlists/package.json @@ -8,7 +8,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1", + "node": "^18.18.2", "pnpm": "^8.9.0" }, "packageManager": "pnpm@8.9.0", From ec38716720c33daa4bc13c4a73cddc57ec8a47fc Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 3 Nov 2023 15:46:24 +0100 Subject: [PATCH 25/51] set node-version to correct in test-setup action --- .github/actions/test-setup/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/test-setup/action.yaml b/.github/actions/test-setup/action.yaml index b37f3e33084..225f040e5e2 100644 --- a/.github/actions/test-setup/action.yaml +++ b/.github/actions/test-setup/action.yaml @@ -2,7 +2,7 @@ name: "Test Setup" inputs: node-version: description: "Node version" - default: 18.14.1 + default: 18.18.2 pnpm-version: description: "PNPM version" default: 8.9.0 @@ -31,4 +31,4 @@ runs: - name: Build run: pnpm build - shell: bash \ No newline at end of file + shell: bash From 99e0982bb3cb4d8c13e3a4f940e28c45cfa1787b Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 3 Nov 2023 15:49:52 +0100 Subject: [PATCH 26/51] lint: removed unused variables --- packages/providers/test/provider.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/providers/test/provider.test.ts b/packages/providers/test/provider.test.ts index 9e74c41a698..21731aa10d5 100644 --- a/packages/providers/test/provider.test.ts +++ b/packages/providers/test/provider.test.ts @@ -22,8 +22,6 @@ import { TransactionResponse } from '../src/transaction-response'; import { fromTai64ToUnix, fromUnixToTai64, sleep } from '../src/utils'; import { messageProofResponse, messageStatusResponse } from './fixtures'; -import { MOCK_CHAIN } from './fixtures/chain'; -import { MOCK_NODE_INFO } from './fixtures/nodeInfo'; // https://stackoverflow.com/a/72885576 jest.mock('@fuel-ts/versions', () => ({ From 66ed47574021bc9a7d405932e4885f2bd9d78e91 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 8 Nov 2023 17:39:25 +0100 Subject: [PATCH 27/51] chore: update all packages to 18.18.2 again --- packages/abi-typegen/package.json | 2 +- packages/address/package.json | 2 +- packages/contract/package.json | 2 +- packages/crypto/package.json | 2 +- packages/errors/package.json | 2 +- packages/fuels/package.json | 2 +- packages/hasher/package.json | 2 +- packages/hdwallet/package.json | 2 +- packages/interfaces/package.json | 2 +- packages/math/package.json | 2 +- packages/merkle/package.json | 2 +- packages/mnemonic/package.json | 2 +- packages/predicate/package.json | 2 +- packages/program/package.json | 2 +- packages/providers/package.json | 2 +- packages/script/package.json | 2 +- packages/signer/package.json | 2 +- packages/transactions/package.json | 2 +- packages/utils/package.json | 2 +- packages/versions/package.json | 2 +- packages/wallet-manager/package.json | 2 +- packages/wallet/package.json | 2 +- packages/wordlists/package.json | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/abi-typegen/package.json b/packages/abi-typegen/package.json index e68c88715eb..02ca375657b 100644 --- a/packages/abi-typegen/package.json +++ b/packages/abi-typegen/package.json @@ -10,7 +10,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/address/package.json b/packages/address/package.json index c08669f7ca4..4c15628e579 100644 --- a/packages/address/package.json +++ b/packages/address/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/contract/package.json b/packages/contract/package.json index 78b8344319a..1c85859780a 100644 --- a/packages/contract/package.json +++ b/packages/contract/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 0e90946ef7d..d2a88ce3251 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "browser": { "./dist/index.mjs": "./dist/index.browser.mjs" diff --git a/packages/errors/package.json b/packages/errors/package.json index 19264085d92..e051b5a36a9 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/fuels/package.json b/packages/fuels/package.json index 5a86384e255..41c9d8f1a84 100644 --- a/packages/fuels/package.json +++ b/packages/fuels/package.json @@ -10,7 +10,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/hasher/package.json b/packages/hasher/package.json index c6e25ceac0f..af580c24403 100644 --- a/packages/hasher/package.json +++ b/packages/hasher/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/hdwallet/package.json b/packages/hdwallet/package.json index 068436869f7..4c39e8b5f8b 100644 --- a/packages/hdwallet/package.json +++ b/packages/hdwallet/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/interfaces/package.json b/packages/interfaces/package.json index d74ba5966b0..d98714466e5 100644 --- a/packages/interfaces/package.json +++ b/packages/interfaces/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/math/package.json b/packages/math/package.json index 025b1edbfe0..7ba4361bb48 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/merkle/package.json b/packages/merkle/package.json index 8179f2815c2..965688439f7 100644 --- a/packages/merkle/package.json +++ b/packages/merkle/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/mnemonic/package.json b/packages/mnemonic/package.json index b4f5c640f1d..0419eed4e7f 100644 --- a/packages/mnemonic/package.json +++ b/packages/mnemonic/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/predicate/package.json b/packages/predicate/package.json index 793750cc2d6..fc48cacf9c1 100644 --- a/packages/predicate/package.json +++ b/packages/predicate/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/program/package.json b/packages/program/package.json index 6049444b521..1ca97104f24 100644 --- a/packages/program/package.json +++ b/packages/program/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/providers/package.json b/packages/providers/package.json index 7ae0d60b881..0fcafaf2cee 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/script/package.json b/packages/script/package.json index d0586d16a43..023578770be 100644 --- a/packages/script/package.json +++ b/packages/script/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/signer/package.json b/packages/signer/package.json index f0528402f14..d59cc0a692e 100644 --- a/packages/signer/package.json +++ b/packages/signer/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/transactions/package.json b/packages/transactions/package.json index c16deff3c0b..ce37cc028d0 100644 --- a/packages/transactions/package.json +++ b/packages/transactions/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/utils/package.json b/packages/utils/package.json index 8cc01cd35a1..f599d9aa54a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/versions/package.json b/packages/versions/package.json index 218ee77ef2d..7c40762eff5 100644 --- a/packages/versions/package.json +++ b/packages/versions/package.json @@ -10,7 +10,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/wallet-manager/package.json b/packages/wallet-manager/package.json index 9b1c3f3811f..554a696d160 100644 --- a/packages/wallet-manager/package.json +++ b/packages/wallet-manager/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 212c675f078..887d7fe8291 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -7,7 +7,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { diff --git a/packages/wordlists/package.json b/packages/wordlists/package.json index 74de290d834..799b973f9a4 100644 --- a/packages/wordlists/package.json +++ b/packages/wordlists/package.json @@ -8,7 +8,7 @@ "module": "dist/index.mjs", "types": "dist/index.d.ts", "engines": { - "node": "^18.14.1" + "node": "^18.18.2" }, "exports": { ".": { From dfe679a519ffd9dbf39178173a8cdf749724726c Mon Sep 17 00:00:00 2001 From: nedsalk Date: Mon, 13 Nov 2023 09:32:20 +0100 Subject: [PATCH 28/51] refactor: change loop style --- packages/providers/src/fuel-graphql-subscriber.ts | 2 +- packages/providers/test/provider.test.ts | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/providers/src/fuel-graphql-subscriber.ts b/packages/providers/src/fuel-graphql-subscriber.ts index e88bb33025e..85a33cfab2d 100644 --- a/packages/providers/src/fuel-graphql-subscriber.ts +++ b/packages/providers/src/fuel-graphql-subscriber.ts @@ -66,7 +66,7 @@ export async function* fuelGraphQLSubscriber({ .body!.pipeThrough(new FuelSubscriptionStream()) .getReader(); - for (;;) { + while (true) { const { value, done } = await subscriptionStreamReader.read(); if (value instanceof FuelError) throw value; yield value; diff --git a/packages/providers/test/provider.test.ts b/packages/providers/test/provider.test.ts index 25a246c0a3c..75f9cd81286 100644 --- a/packages/providers/test/provider.test.ts +++ b/packages/providers/test/provider.test.ts @@ -204,7 +204,7 @@ describe('Provider', () => { it('can change the provider url of the current instance', async () => { const providerUrl1 = FUEL_NETWORK_URL; - const providerUrl2 = 'https://beta-4.fuel.network/graphql'; + const providerUrl2 = 'http://127.0.0.1:8080/graphql'; const provider = await Provider.create(providerUrl1, { fetch: (url: string, options: FetchRequestOptions) => @@ -214,9 +214,9 @@ describe('Provider', () => { expect(provider.url).toBe(providerUrl1); expect(await provider.getVersion()).toEqual(providerUrl1); - const spyFetchChainAndNodeInfo = jest.spyOn(Provider.prototype, 'fetchChainAndNodeInfo'); - const spyFetchChain = jest.spyOn(Provider.prototype, 'fetchChain'); - const spyFetchNode = jest.spyOn(Provider.prototype, 'fetchNode'); + const spyFetchChainAndNodeInfo = jest + .spyOn(Provider.prototype, 'fetchChainAndNodeInfo') + .mockImplementation(); await provider.connect(providerUrl2); expect(provider.url).toBe(providerUrl2); @@ -224,8 +224,6 @@ describe('Provider', () => { expect(await provider.getVersion()).toEqual(providerUrl2); expect(spyFetchChainAndNodeInfo).toHaveBeenCalledTimes(1); - expect(spyFetchChain).toHaveBeenCalledTimes(1); - expect(spyFetchNode).toHaveBeenCalledTimes(1); }); it('can accept a custom fetch function', async () => { From ab7772eef1d33217a315dbdfeaea457a39291fe5 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Mon, 20 Nov 2023 12:44:26 +0100 Subject: [PATCH 29/51] fix: linting and formatting --- packages/providers/src/fuel-graphql-subscriber.ts | 12 +++++++++--- .../src/transaction-response/transaction-response.ts | 8 ++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/providers/src/fuel-graphql-subscriber.ts b/packages/providers/src/fuel-graphql-subscriber.ts index 85a33cfab2d..fb1f80f9a18 100644 --- a/packages/providers/src/fuel-graphql-subscriber.ts +++ b/packages/providers/src/fuel-graphql-subscriber.ts @@ -36,7 +36,9 @@ class FuelSubscriptionStream implements TransformStream { errors.map((err) => err.message).join('\n\n') ) ); - } else this.readableStreamController.enqueue(data); + } else { + this.readableStreamController.enqueue(data); + } } }, }); @@ -68,8 +70,12 @@ export async function* fuelGraphQLSubscriber({ while (true) { const { value, done } = await subscriptionStreamReader.read(); - if (value instanceof FuelError) throw value; + if (value instanceof FuelError) { + throw value; + } yield value; - if (done) break; + if (done) { + break; + } } } diff --git a/packages/providers/src/transaction-response/transaction-response.ts b/packages/providers/src/transaction-response/transaction-response.ts index a3bd3c850d7..f1b936d9c09 100644 --- a/packages/providers/src/transaction-response/transaction-response.ts +++ b/packages/providers/src/transaction-response/transaction-response.ts @@ -128,7 +128,9 @@ export class TransactionResponse { for await (const { statusChange } of this.provider.operations.statusChange({ transactionId: this.id, })) { - if (statusChange) break; + if (statusChange) { + break; + } } return this.fetch(); @@ -203,7 +205,9 @@ export class TransactionResponse { for await (const { statusChange } of this.provider.operations.statusChange({ transactionId: this.id, })) { - if (statusChange.type !== 'SubmittedStatus') break; + if (statusChange.type !== 'SubmittedStatus') { + break; + } } await this.fetch(); From bc8e16e33b9421215137c70ef74a8512e4d7c93f Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 15 Nov 2023 11:11:59 +0100 Subject: [PATCH 30/51] feat: retry-config --- packages/providers/src/retry-config.ts | 36 ++++++++++++++++++++++++ packages/providers/test/retry.test.ts | 39 ++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 packages/providers/src/retry-config.ts create mode 100644 packages/providers/test/retry.test.ts diff --git a/packages/providers/src/retry-config.ts b/packages/providers/src/retry-config.ts new file mode 100644 index 00000000000..eadd4dac6e9 --- /dev/null +++ b/packages/providers/src/retry-config.ts @@ -0,0 +1,36 @@ +type Backoff = 'linear' | 'exponential' | 'fixed'; + +interface RetryOptions { + maxAttempts: number; + backoff: Backoff; + duration: number; +} + +export class RetryConfig { + private backoff: Backoff; + private duration: number; + maxAttempts: number; + /** + * + */ + constructor(options: RetryOptions) { + if (options.maxAttempts <= 0) throw new Error(); + this.backoff = options.backoff; + this.duration = options.duration; + this.maxAttempts = options.maxAttempts; + } + + waitDuration(attempt: number) { + if (attempt === 1) return this.duration; + switch (this.backoff) { + case 'linear': + return this.duration * (attempt + 1); + case 'exponential': + return this.duration * (2 ^ attempt); + case 'fixed': + return this.duration; + default: + throw new Error(); + } + } +} diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts new file mode 100644 index 00000000000..042745d00e2 --- /dev/null +++ b/packages/providers/test/retry.test.ts @@ -0,0 +1,39 @@ +import { RetryConfig } from '../src/retry-config'; + +describe('Retry mechanism wait durations', () => { + test('fixed backoffs', () => { + const maxAttempts = 5; + const duration = 1000; + const config = new RetryConfig({ maxAttempts, backoff: 'fixed', duration }); + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const waitDuration = config.waitDuration(attempt); + expect(waitDuration).toEqual(duration); + } + }); + + test('linear backoffs', () => { + const maxAttempts = 5; + const duration = 1000; + const config = new RetryConfig({ maxAttempts, backoff: 'linear', duration }); + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const waitDuration = config.waitDuration(attempt); + const expectedDuration = attempt === 1 ? duration : duration * (attempt + 1); + expect(waitDuration).toEqual(expectedDuration); + } + }); + + test('exponential backoffs', () => { + const maxAttempts = 5; + const duration = 1000; + const config = new RetryConfig({ maxAttempts, backoff: 'linear', duration }); + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const waitDuration = config.waitDuration(attempt); + const expectedDuration = attempt === 1 ? duration : duration * (attempt + 1); + + expect(waitDuration).toEqual(expectedDuration); + } + }); +}); From 7823f174c794e9ad9f4ffeb37fed84dfd0feb5e5 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 17 Nov 2023 11:26:26 +0100 Subject: [PATCH 31/51] feat: implemented retries on provider level --- packages/providers/src/provider.ts | 15 ++- packages/providers/src/retry-config.ts | 44 +++++++- packages/providers/test/retry.test.ts | 136 ++++++++++++++++++++----- 3 files changed, 160 insertions(+), 35 deletions(-) diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index 0caf75d1f81..131726a4eff 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -28,6 +28,7 @@ import { fuelGraphQLSubscriber } from './fuel-graphql-subscriber'; import { MemoryCache } from './memory-cache'; import type { Message, MessageCoin, MessageProof, MessageStatus } from './message'; import type { ExcludeResourcesOption, Resource } from './resource'; +import { RetryConfig } from './retry-config'; import type { TransactionRequestLike, TransactionRequest, @@ -211,6 +212,7 @@ export type ProviderOptions = { ) => Promise; timeout?: number; cacheUtxo?: number; + retryConfig?: RetryConfig; }; /** @@ -249,17 +251,20 @@ export default class Provider { timeout: undefined, cacheUtxo: undefined, fetch: undefined, + retryConfig: undefined, }; private static getFetchFn(options: ProviderOptions) { return options.fetch !== undefined ? options.fetch : (url: string, request: FetchRequestOptions) => - fetch(url, { - ...request, - signal: - options.timeout !== undefined ? AbortSignal.timeout(options.timeout) : undefined, - }); + RetryConfig.retryable(options.retryConfig, () => + fetch(url, { + ...request, + signal: + options.timeout !== undefined ? AbortSignal.timeout(options.timeout) : undefined, + }) + ); } /** diff --git a/packages/providers/src/retry-config.ts b/packages/providers/src/retry-config.ts index eadd4dac6e9..54ae90b3693 100644 --- a/packages/providers/src/retry-config.ts +++ b/packages/providers/src/retry-config.ts @@ -1,14 +1,28 @@ +import { sleep } from './utils'; + type Backoff = 'linear' | 'exponential' | 'fixed'; interface RetryOptions { + /** + * Amount of attempts to retry before failing the call. + */ maxAttempts: number; + /** + * Backoff strategy to use when retrying. + */ backoff: Backoff; + /** + * Base duration for backoff strategy. + */ duration: number; } export class RetryConfig { private backoff: Backoff; private duration: number; + /** + * Amount of attempts to try after first call fails. + */ maxAttempts: number; /** * @@ -21,16 +35,40 @@ export class RetryConfig { } waitDuration(attempt: number) { - if (attempt === 1) return this.duration; switch (this.backoff) { case 'linear': - return this.duration * (attempt + 1); + return this.duration * attempt; case 'exponential': - return this.duration * (2 ^ attempt); + return this.duration * (2 ^ (attempt - 1)); case 'fixed': return this.duration; default: throw new Error(); } } + + static async retryable Promise>( + config: RetryConfig | undefined, + call: Call + ) { + if (config === undefined) return call(); + + let retryAttempt = 0; + do { + try { + return await call(); + } catch (e: unknown) { + const error = e as Error & { cause?: { code?: string } }; + + if (error.cause?.code !== 'ECONNREFUSED') throw e; + + retryAttempt += 1; + + if (retryAttempt > config.maxAttempts) throw e; + + await sleep(config.waitDuration(retryAttempt)); + } + // eslint-disable-next-line no-constant-condition + } while (true); + } } diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index 042745d00e2..13ed4b6a08f 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -1,39 +1,121 @@ +import { safeExec } from '@fuel-ts/errors/test-utils'; + +import Provider from '../src/provider'; import { RetryConfig } from '../src/retry-config'; -describe('Retry mechanism wait durations', () => { - test('fixed backoffs', () => { - const maxAttempts = 5; - const duration = 1000; - const config = new RetryConfig({ maxAttempts, backoff: 'fixed', duration }); +const FUEL_NETWORK_URL = 'http://127.0.0.1:4000/graphql'; - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - const waitDuration = config.waitDuration(attempt); - expect(waitDuration).toEqual(duration); - } +describe('Retries correctly', () => { + afterEach(() => { + jest.clearAllMocks(); }); - test('linear backoffs', () => { - const maxAttempts = 5; - const duration = 1000; - const config = new RetryConfig({ maxAttempts, backoff: 'linear', duration }); - - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - const waitDuration = config.waitDuration(attempt); - const expectedDuration = attempt === 1 ? duration : duration * (attempt + 1); - expect(waitDuration).toEqual(expectedDuration); - } + test.each([ + { + backoff: 'fixed' as const, + backoffFn: (duration: number) => duration, + }, + { + backoff: 'linear' as const, + backoffFn: (duration: number, attempt: number) => duration * attempt, + }, + { + backoff: 'exponential' as const, + backoffFn: (duration: number, attempt: number) => duration * (2 ^ (attempt - 1)), + }, + ])('retries until successful: $backoff backoff', async ({ backoff, backoffFn }) => { + const maxAttempts = 4; + const duration = 150; + + const retryConfig = new RetryConfig({ maxAttempts, duration, backoff }); + + const provider = await Provider.create(FUEL_NETWORK_URL, { retryConfig }); + + const expectedChainInfo = await provider.operations.getChain(); + + const fetchSpy = jest.spyOn(global, 'fetch'); + + const callTimes: number[] = []; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TS is throwing error when test is run, but not in IDE + fetchSpy.mockImplementation((input: RequestInfo | URL, init: RequestInit | undefined) => { + // const time = Date.now(); + + // if (fetchSpy.mock.calls.length === 1) { + // initialCallTime = time; + // } else { + // retryCallTimes.push(time); + // } + + callTimes.push(Date.now()); + + if (fetchSpy.mock.calls.length <= maxAttempts) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore asd + const error = new Error(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TS is throwing error when test is run, but not in IDE + error.cause = { + code: 'ECONNREFUSED', + }; + + throw error; + } + + fetchSpy.mockRestore(); + + return fetch(input, init); + }); + + const chainInfo = await provider.operations.getChain(); + + expect(chainInfo).toEqual(expectedChainInfo); + expect(callTimes.length - 1).toBe(maxAttempts); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it + + callTimes.forEach((callTime, index) => { + if (index === 0) return; // initial call doesn't count as it's not a retry + + const waitTime = callTime - callTimes[index - 1]; + + const expectedWaitTime = backoffFn(duration, index); + + // in one test run the waitTime was 1ms less than the expectedWaitTime + // meaning that the call happened before the wait duration expired + // this might be something related to the event loop and how it schedules setTimeouts + // expectedWaitTime minus 5ms seems like reasonable to allow + expect(waitTime).toBeGreaterThanOrEqual(expectedWaitTime - 5); + expect(waitTime).toBeLessThanOrEqual(expectedWaitTime + 10); + }); }); - test('exponential backoffs', () => { + test('throws if last attempt fails', async () => { const maxAttempts = 5; - const duration = 1000; - const config = new RetryConfig({ maxAttempts, backoff: 'linear', duration }); + const duration = 100; + + const retryConfig = new RetryConfig({ maxAttempts, duration, backoff: 'fixed' }); + + const provider = await Provider.create(FUEL_NETWORK_URL, { retryConfig }); + + const fetchSpy = jest + .spyOn(global, 'fetch') + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TS is throwing error when test is run, but not in IDE + .mockImplementation(() => { + const error = new Error(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TS is throwing error when test is run, but not in IDE + error.cause = { + code: 'ECONNREFUSED', + }; + + throw error; + }); - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - const waitDuration = config.waitDuration(attempt); - const expectedDuration = attempt === 1 ? duration : duration * (attempt + 1); + const { error } = await safeExec(() => provider.operations.getChain()); - expect(waitDuration).toEqual(expectedDuration); - } + expect(error).toMatchObject({ cause: { code: 'ECONNREFUSED' } }); + // the added one is for the initial call which isn't considered a retry attempt + expect(fetchSpy).toHaveBeenCalledTimes(maxAttempts + 1); }); }); From b2a72460f1c835f33249d21cb63f11f9dddba5ea Mon Sep 17 00:00:00 2001 From: nedsalk Date: Tue, 21 Nov 2023 11:22:29 +0100 Subject: [PATCH 32/51] refactor: into recursion --- packages/providers/src/call-retrier.ts | 57 ++++++++++++++++++++ packages/providers/src/provider.ts | 22 ++++---- packages/providers/src/retry-config.ts | 74 -------------------------- packages/providers/test/retry.test.ts | 19 ++----- 4 files changed, 74 insertions(+), 98 deletions(-) create mode 100644 packages/providers/src/call-retrier.ts delete mode 100644 packages/providers/src/retry-config.ts diff --git a/packages/providers/src/call-retrier.ts b/packages/providers/src/call-retrier.ts new file mode 100644 index 00000000000..9c06271f482 --- /dev/null +++ b/packages/providers/src/call-retrier.ts @@ -0,0 +1,57 @@ +import { sleep } from './utils'; + +type Backoff = 'linear' | 'exponential' | 'fixed'; + +export interface RetryOptions { + /** + * Amount of attempts to retry before failing the call. + */ + maxAttempts: number; + /** + * Backoff strategy to use when retrying. + */ + backoff: Backoff; + /** + * Base duration for backoff strategy. + */ + baseDuration: number; +} + +function getWaitDuration(options: RetryOptions, attempt: number) { + if (attempt === 0) return options.baseDuration; + + switch (options.backoff) { + case 'linear': + return options.baseDuration * attempt; + case 'exponential': + return options.baseDuration * (2 ^ (attempt - 1)); + case 'fixed': + return options.baseDuration; + default: + throw new Error(); + } +} + +export async function retrier( + call: () => Promise, + options: RetryOptions | undefined, + retryAttempt: number = 0 +) { + if (options === undefined) return call(); + + try { + return await call(); + } catch (e: unknown) { + const error = e as Error & { cause?: { code?: string } }; + + if (error.cause?.code !== 'ECONNREFUSED') throw e; + + if (retryAttempt === options.maxAttempts) throw e; + + // eslint-disable-next-line no-param-reassign + retryAttempt += 1; + + await sleep(getWaitDuration(options, retryAttempt)); + return retrier(call, options, retryAttempt); + } +} diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index 131726a4eff..57136c2147f 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -21,6 +21,8 @@ import type { GqlChainInfoFragmentFragment, GqlGetBlocksQueryVariables, } from './__generated__/operations'; +import type { RetryOptions } from './call-retrier'; +import { retrier } from './call-retrier'; import type { Coin } from './coin'; import type { CoinQuantity, CoinQuantityLike } from './coin-quantity'; import { coinQuantityfy } from './coin-quantity'; @@ -28,7 +30,6 @@ import { fuelGraphQLSubscriber } from './fuel-graphql-subscriber'; import { MemoryCache } from './memory-cache'; import type { Message, MessageCoin, MessageProof, MessageStatus } from './message'; import type { ExcludeResourcesOption, Resource } from './resource'; -import { RetryConfig } from './retry-config'; import type { TransactionRequestLike, TransactionRequest, @@ -212,7 +213,7 @@ export type ProviderOptions = { ) => Promise; timeout?: number; cacheUtxo?: number; - retryConfig?: RetryConfig; + retryOptions?: RetryOptions; }; /** @@ -251,20 +252,23 @@ export default class Provider { timeout: undefined, cacheUtxo: undefined, fetch: undefined, - retryConfig: undefined, + retryOptions: undefined, }; private static getFetchFn(options: ProviderOptions) { - return options.fetch !== undefined - ? options.fetch - : (url: string, request: FetchRequestOptions) => - RetryConfig.retryable(options.retryConfig, () => + const fetchFn = + options.fetch !== undefined + ? options.fetch + : (url: string, request: FetchRequestOptions) => fetch(url, { ...request, signal: options.timeout !== undefined ? AbortSignal.timeout(options.timeout) : undefined, - }) - ); + }); + + type FetchParams = Parameters>; + + return (...args: FetchParams) => retrier(() => fetchFn(...args), options.retryOptions); } /** diff --git a/packages/providers/src/retry-config.ts b/packages/providers/src/retry-config.ts deleted file mode 100644 index 54ae90b3693..00000000000 --- a/packages/providers/src/retry-config.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { sleep } from './utils'; - -type Backoff = 'linear' | 'exponential' | 'fixed'; - -interface RetryOptions { - /** - * Amount of attempts to retry before failing the call. - */ - maxAttempts: number; - /** - * Backoff strategy to use when retrying. - */ - backoff: Backoff; - /** - * Base duration for backoff strategy. - */ - duration: number; -} - -export class RetryConfig { - private backoff: Backoff; - private duration: number; - /** - * Amount of attempts to try after first call fails. - */ - maxAttempts: number; - /** - * - */ - constructor(options: RetryOptions) { - if (options.maxAttempts <= 0) throw new Error(); - this.backoff = options.backoff; - this.duration = options.duration; - this.maxAttempts = options.maxAttempts; - } - - waitDuration(attempt: number) { - switch (this.backoff) { - case 'linear': - return this.duration * attempt; - case 'exponential': - return this.duration * (2 ^ (attempt - 1)); - case 'fixed': - return this.duration; - default: - throw new Error(); - } - } - - static async retryable Promise>( - config: RetryConfig | undefined, - call: Call - ) { - if (config === undefined) return call(); - - let retryAttempt = 0; - do { - try { - return await call(); - } catch (e: unknown) { - const error = e as Error & { cause?: { code?: string } }; - - if (error.cause?.code !== 'ECONNREFUSED') throw e; - - retryAttempt += 1; - - if (retryAttempt > config.maxAttempts) throw e; - - await sleep(config.waitDuration(retryAttempt)); - } - // eslint-disable-next-line no-constant-condition - } while (true); - } -} diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index 13ed4b6a08f..74e728f96de 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -1,7 +1,6 @@ import { safeExec } from '@fuel-ts/errors/test-utils'; import Provider from '../src/provider'; -import { RetryConfig } from '../src/retry-config'; const FUEL_NETWORK_URL = 'http://127.0.0.1:4000/graphql'; @@ -27,9 +26,9 @@ describe('Retries correctly', () => { const maxAttempts = 4; const duration = 150; - const retryConfig = new RetryConfig({ maxAttempts, duration, backoff }); + const retryOptions = { maxAttempts, baseDuration: duration, backoff }; - const provider = await Provider.create(FUEL_NETWORK_URL, { retryConfig }); + const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); const expectedChainInfo = await provider.operations.getChain(); @@ -40,19 +39,9 @@ describe('Retries correctly', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore TS is throwing error when test is run, but not in IDE fetchSpy.mockImplementation((input: RequestInfo | URL, init: RequestInit | undefined) => { - // const time = Date.now(); - - // if (fetchSpy.mock.calls.length === 1) { - // initialCallTime = time; - // } else { - // retryCallTimes.push(time); - // } - callTimes.push(Date.now()); if (fetchSpy.mock.calls.length <= maxAttempts) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore asd const error = new Error(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore TS is throwing error when test is run, but not in IDE @@ -93,9 +82,9 @@ describe('Retries correctly', () => { const maxAttempts = 5; const duration = 100; - const retryConfig = new RetryConfig({ maxAttempts, duration, backoff: 'fixed' }); + const retryOptions = { maxAttempts, baseDuration: duration, backoff: 'fixed' as const }; - const provider = await Provider.create(FUEL_NETWORK_URL, { retryConfig }); + const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); const fetchSpy = jest .spyOn(global, 'fetch') From a3a6f501043e16a113aaf69bf5472185a5a034ad Mon Sep 17 00:00:00 2001 From: nedsalk Date: Tue, 21 Nov 2023 13:05:42 +0100 Subject: [PATCH 33/51] fix: formatting --- packages/providers/src/call-retrier.ts | 16 ++++++++++++---- packages/providers/test/retry.test.ts | 4 +++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/providers/src/call-retrier.ts b/packages/providers/src/call-retrier.ts index 9c06271f482..0eddb14c9ca 100644 --- a/packages/providers/src/call-retrier.ts +++ b/packages/providers/src/call-retrier.ts @@ -18,7 +18,9 @@ export interface RetryOptions { } function getWaitDuration(options: RetryOptions, attempt: number) { - if (attempt === 0) return options.baseDuration; + if (attempt === 0) { + return options.baseDuration; + } switch (options.backoff) { case 'linear': @@ -37,16 +39,22 @@ export async function retrier( options: RetryOptions | undefined, retryAttempt: number = 0 ) { - if (options === undefined) return call(); + if (options === undefined) { + return call(); + } try { return await call(); } catch (e: unknown) { const error = e as Error & { cause?: { code?: string } }; - if (error.cause?.code !== 'ECONNREFUSED') throw e; + if (error.cause?.code !== 'ECONNREFUSED') { + throw e; + } - if (retryAttempt === options.maxAttempts) throw e; + if (retryAttempt === options.maxAttempts) { + throw e; + } // eslint-disable-next-line no-param-reassign retryAttempt += 1; diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index 74e728f96de..0f67c9ae3e1 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -63,7 +63,9 @@ describe('Retries correctly', () => { expect(callTimes.length - 1).toBe(maxAttempts); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it callTimes.forEach((callTime, index) => { - if (index === 0) return; // initial call doesn't count as it's not a retry + if (index === 0) { + return; + } // initial call doesn't count as it's not a retry const waitTime = callTime - callTimes[index - 1]; From 403ccf30b93feb23380b22740a15ff6932dd56df Mon Sep 17 00:00:00 2001 From: nedsalk Date: Sat, 2 Dec 2023 11:48:04 +0100 Subject: [PATCH 34/51] refactor: tests --- packages/providers/test/retry.test.ts | 131 ++++++++++++++++---------- 1 file changed, 79 insertions(+), 52 deletions(-) diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index 0f67c9ae3e1..7872209c930 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -4,86 +4,113 @@ import Provider from '../src/provider'; const FUEL_NETWORK_URL = 'http://127.0.0.1:4000/graphql'; +function mockFetch(maxAttempts: number, callTimes: number[]) { + const fetchSpy = jest.spyOn(global, 'fetch'); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TS is throwing error when test is run, but not in IDE + fetchSpy.mockImplementation((input: RequestInfo | URL, init: RequestInit | undefined) => { + callTimes.push(Date.now()); + + if (fetchSpy.mock.calls.length <= maxAttempts) { + const error = new Error(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TS is throwing error when test is run, but not in IDE + error.cause = { + code: 'ECONNREFUSED', + }; + + throw error; + } + + fetchSpy.mockRestore(); + + return fetch(input, init); + }); +} + describe('Retries correctly', () => { afterEach(() => { jest.clearAllMocks(); }); - test.each([ - { - backoff: 'fixed' as const, - backoffFn: (duration: number) => duration, - }, - { - backoff: 'linear' as const, - backoffFn: (duration: number, attempt: number) => duration * attempt, - }, - { - backoff: 'exponential' as const, - backoffFn: (duration: number, attempt: number) => duration * (2 ^ (attempt - 1)), - }, - ])('retries until successful: $backoff backoff', async ({ backoff, backoffFn }) => { - const maxAttempts = 4; - const duration = 150; - - const retryOptions = { maxAttempts, baseDuration: duration, backoff }; + const maxAttempts = 4; + const duration = 150; + + function assertBackoff(callTime: number, index: number, arr: number[], expectedWaitTime: number) { + if (index === 0) { + return; + } // initial call doesn't count as it's not a retry + + const waitTime = callTime - arr[index - 1]; + + // in one test run the waitTime was 1ms less than the expectedWaitTime + // meaning that the call happened before the wait duration expired + // this might be something related to the event loop and how it schedules setTimeouts + // expectedWaitTime minus 5ms seems like reasonable to allow + expect(waitTime).toBeGreaterThanOrEqual(expectedWaitTime - 5); + expect(waitTime).toBeLessThanOrEqual(expectedWaitTime + 10); + } + + test('fixed backoff', async () => { + const retryOptions = { maxAttempts, baseDuration: duration, backoff: 'fixed' as const }; const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); + const callTimes: number[] = []; + + mockFetch(maxAttempts, callTimes); const expectedChainInfo = await provider.operations.getChain(); - const fetchSpy = jest.spyOn(global, 'fetch'); + const chainInfo = await provider.operations.getChain(); - const callTimes: number[] = []; + expect(chainInfo).toEqual(expectedChainInfo); + expect(callTimes.length - 1).toBe(maxAttempts); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore TS is throwing error when test is run, but not in IDE - fetchSpy.mockImplementation((input: RequestInfo | URL, init: RequestInit | undefined) => { - callTimes.push(Date.now()); + callTimes.forEach((callTime, index) => assertBackoff(callTime, index, callTimes, duration)); + }); - if (fetchSpy.mock.calls.length <= maxAttempts) { - const error = new Error(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore TS is throwing error when test is run, but not in IDE - error.cause = { - code: 'ECONNREFUSED', - }; + test('linear backoff', async () => { + const retryOptions = { maxAttempts, baseDuration: duration, backoff: 'linear' as const }; - throw error; - } + const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); + const callTimes: number[] = []; - fetchSpy.mockRestore(); + mockFetch(maxAttempts, callTimes); - return fetch(input, init); - }); + const expectedChainInfo = await provider.operations.getChain(); const chainInfo = await provider.operations.getChain(); expect(chainInfo).toEqual(expectedChainInfo); expect(callTimes.length - 1).toBe(maxAttempts); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it - callTimes.forEach((callTime, index) => { - if (index === 0) { - return; - } // initial call doesn't count as it's not a retry + callTimes.forEach((callTime, index) => + assertBackoff(callTime, index, callTimes, duration * index) + ); + }); - const waitTime = callTime - callTimes[index - 1]; + test('exponential backoff', async () => { + const retryOptions = { maxAttempts, baseDuration: duration, backoff: 'exponential' as const }; - const expectedWaitTime = backoffFn(duration, index); + const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); + const callTimes: number[] = []; + + mockFetch(maxAttempts, callTimes); + + const expectedChainInfo = await provider.operations.getChain(); + + const chainInfo = await provider.operations.getChain(); - // in one test run the waitTime was 1ms less than the expectedWaitTime - // meaning that the call happened before the wait duration expired - // this might be something related to the event loop and how it schedules setTimeouts - // expectedWaitTime minus 5ms seems like reasonable to allow - expect(waitTime).toBeGreaterThanOrEqual(expectedWaitTime - 5); - expect(waitTime).toBeLessThanOrEqual(expectedWaitTime + 10); - }); + expect(chainInfo).toEqual(expectedChainInfo); + expect(callTimes.length - 1).toBe(maxAttempts); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it + + callTimes.forEach((callTime, index) => + assertBackoff(callTime, index, callTimes, duration * duration * (2 ^ (index - 1))) + ); }); test('throws if last attempt fails', async () => { - const maxAttempts = 5; - const duration = 100; - const retryOptions = { maxAttempts, baseDuration: duration, backoff: 'fixed' as const }; const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); From 253599b685e51003d510b11e094ea45190b29802 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Sat, 2 Dec 2023 11:52:10 +0100 Subject: [PATCH 35/51] fix: failing test --- packages/providers/test/retry.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index 7872209c930..7804a19b6f0 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -106,7 +106,7 @@ describe('Retries correctly', () => { expect(callTimes.length - 1).toBe(maxAttempts); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it callTimes.forEach((callTime, index) => - assertBackoff(callTime, index, callTimes, duration * duration * (2 ^ (index - 1))) + assertBackoff(callTime, index, callTimes, duration * (2 ^ (index - 1))) ); }); From 6d1c82ad647425dda76de80c754d0f9834932f77 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Sat, 2 Dec 2023 12:54:46 +0100 Subject: [PATCH 36/51] comment --- packages/providers/test/retry.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index 7804a19b6f0..37c8c692601 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -2,6 +2,7 @@ import { safeExec } from '@fuel-ts/errors/test-utils'; import Provider from '../src/provider'; +// TODO: Figure out a way to import this constant from `@fuel-ts/wallet/configs` const FUEL_NETWORK_URL = 'http://127.0.0.1:4000/graphql'; function mockFetch(maxAttempts: number, callTimes: number[]) { From 8c72b1b7d6852e10974785b5b36693de2a2df9ff Mon Sep 17 00:00:00 2001 From: nedsalk Date: Sat, 2 Dec 2023 12:58:00 +0100 Subject: [PATCH 37/51] cleanup ts comments --- packages/providers/test/retry.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index 37c8c692601..f6da7635db1 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -8,15 +8,13 @@ const FUEL_NETWORK_URL = 'http://127.0.0.1:4000/graphql'; function mockFetch(maxAttempts: number, callTimes: number[]) { const fetchSpy = jest.spyOn(global, 'fetch'); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore TS is throwing error when test is run, but not in IDE - fetchSpy.mockImplementation((input: RequestInfo | URL, init: RequestInit | undefined) => { + fetchSpy.mockImplementation((...args: unknown[]) => { callTimes.push(Date.now()); if (fetchSpy.mock.calls.length <= maxAttempts) { const error = new Error(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore TS is throwing error when test is run, but not in IDE + // @ts-ignore TS is not happy with this property, but it works. ts-expect-error doesn't work for some reason, so I chose ts-ignore error.cause = { code: 'ECONNREFUSED', }; @@ -26,7 +24,7 @@ function mockFetch(maxAttempts: number, callTimes: number[]) { fetchSpy.mockRestore(); - return fetch(input, init); + return fetch(args[0] as URL, args[1] as RequestInit); }); } From df88cf322bfcd110c714aa161b006a2e7764d838 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 20 Dec 2023 11:26:23 +0100 Subject: [PATCH 38/51] fix: formatting --- packages/providers/test/provider.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/providers/test/provider.test.ts b/packages/providers/test/provider.test.ts index f089486619e..8a2f21967b3 100644 --- a/packages/providers/test/provider.test.ts +++ b/packages/providers/test/provider.test.ts @@ -17,7 +17,7 @@ import type { CoinTransactionRequestInput, MessageTransactionRequestInput, } from '../src/transaction-request'; -import { ScriptTransactionRequest , CreateTransactionRequest } from '../src/transaction-request'; +import { ScriptTransactionRequest, CreateTransactionRequest } from '../src/transaction-request'; import { TransactionResponse } from '../src/transaction-response'; import { fromTai64ToUnix, fromUnixToTai64, sleep } from '../src/utils'; import * as gasMod from '../src/utils/gas'; From cb7db5347765fd9bc9c520859739e758b9723ea3 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Thu, 21 Dec 2023 19:00:30 +0100 Subject: [PATCH 39/51] fix: remove eslint and tsignores --- packages/providers/test/retry.test.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index f6da7635db1..db724c322ae 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -6,7 +6,7 @@ import Provider from '../src/provider'; const FUEL_NETWORK_URL = 'http://127.0.0.1:4000/graphql'; function mockFetch(maxAttempts: number, callTimes: number[]) { - const fetchSpy = jest.spyOn(global, 'fetch'); + const fetchSpy = vi.spyOn(global, 'fetch'); fetchSpy.mockImplementation((...args: unknown[]) => { callTimes.push(Date.now()); @@ -30,7 +30,7 @@ function mockFetch(maxAttempts: number, callTimes: number[]) { describe('Retries correctly', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); const maxAttempts = 4; @@ -114,20 +114,14 @@ describe('Retries correctly', () => { const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); - const fetchSpy = jest - .spyOn(global, 'fetch') - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore TS is throwing error when test is run, but not in IDE - .mockImplementation(() => { - const error = new Error(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore TS is throwing error when test is run, but not in IDE - error.cause = { - code: 'ECONNREFUSED', - }; - - throw error; - }); + const fetchSpy = vi.spyOn(global, 'fetch').mockImplementation((...args: unknown[]) => { + const error = new Error() as Error & { cause: { code: string } }; + error.cause = { + code: 'ECONNREFUSED', + }; + + throw error; + }); const { error } = await safeExec(() => provider.operations.getChain()); From bd36d247b001e6b28d86bbd22bdc24c8cc364600 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Thu, 21 Dec 2023 19:03:25 +0100 Subject: [PATCH 40/51] remove unnecessary ..args --- packages/providers/test/retry.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index db724c322ae..db2ec9f8ceb 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -114,7 +114,7 @@ describe('Retries correctly', () => { const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); - const fetchSpy = vi.spyOn(global, 'fetch').mockImplementation((...args: unknown[]) => { + const fetchSpy = vi.spyOn(global, 'fetch').mockImplementation(() => { const error = new Error() as Error & { cause: { code: string } }; error.cause = { code: 'ECONNREFUSED', From 36ab4b9d20625b4d9161b7da77f6be3acec5ff99 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 22 Dec 2023 08:15:34 +0100 Subject: [PATCH 41/51] set default values and rename to maxRetries, refactor logic into retrier --- packages/providers/src/call-retrier.ts | 64 ++++++++++++++------------ packages/providers/src/provider.ts | 27 ++++++----- packages/providers/test/retry.test.ts | 33 ++++++++----- 3 files changed, 68 insertions(+), 56 deletions(-) diff --git a/packages/providers/src/call-retrier.ts b/packages/providers/src/call-retrier.ts index 0eddb14c9ca..41148c4c275 100644 --- a/packages/providers/src/call-retrier.ts +++ b/packages/providers/src/call-retrier.ts @@ -1,3 +1,4 @@ +import type { ProviderOptions } from './provider'; import { sleep } from './utils'; type Backoff = 'linear' | 'exponential' | 'fixed'; @@ -6,60 +7,63 @@ export interface RetryOptions { /** * Amount of attempts to retry before failing the call. */ - maxAttempts: number; + maxRetries: number; /** * Backoff strategy to use when retrying. */ - backoff: Backoff; + backoff?: Backoff; /** * Base duration for backoff strategy. */ - baseDuration: number; + baseDuration?: number; } function getWaitDuration(options: RetryOptions, attempt: number) { + const duration = options.baseDuration ?? 150; + if (attempt === 0) { - return options.baseDuration; + return duration; } switch (options.backoff) { case 'linear': - return options.baseDuration * attempt; - case 'exponential': - return options.baseDuration * (2 ^ (attempt - 1)); + return duration * attempt; case 'fixed': - return options.baseDuration; + return duration; + case 'exponential': default: - throw new Error(); + return duration * (2 ^ (attempt - 1)); } } -export async function retrier( - call: () => Promise, +export function retrier( + fetchFn: NonNullable, options: RetryOptions | undefined, retryAttempt: number = 0 -) { - if (options === undefined) { - return call(); - } +): NonNullable { + return async (...args) => { + if (options === undefined) { + return fetchFn(...args); + } - try { - return await call(); - } catch (e: unknown) { - const error = e as Error & { cause?: { code?: string } }; + try { + return await fetchFn(...args); + } catch (e: unknown) { + const error = e as Error & { cause?: { code?: string } }; - if (error.cause?.code !== 'ECONNREFUSED') { - throw e; - } + if (error.cause?.code !== 'ECONNREFUSED') { + throw e; + } - if (retryAttempt === options.maxAttempts) { - throw e; - } + if (retryAttempt === options.maxRetries) { + throw e; + } - // eslint-disable-next-line no-param-reassign - retryAttempt += 1; + // eslint-disable-next-line no-param-reassign + retryAttempt += 1; - await sleep(getWaitDuration(options, retryAttempt)); - return retrier(call, options, retryAttempt); - } + await sleep(getWaitDuration(options, retryAttempt)); + return retrier(fetchFn, options, retryAttempt)(...args); + } + }; } diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index 5b27395c224..2187260b00b 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -285,20 +285,19 @@ export default class Provider { retryOptions: undefined, }; - private static getFetchFn(options: ProviderOptions) { - const fetchFn = - options.fetch !== undefined - ? options.fetch - : (url: string, request: FetchRequestOptions) => - fetch(url, { - ...request, - signal: - options.timeout !== undefined ? AbortSignal.timeout(options.timeout) : undefined, - }); - - type FetchParams = Parameters>; - - return (...args: FetchParams) => retrier(() => fetchFn(...args), options.retryOptions); + private static getFetchFn(options: ProviderOptions): NonNullable { + return retrier((...args) => { + if (options.fetch) { + return options.fetch(...args); + } + + const url = args[0]; + const request = args[1]; + return fetch(url, { + ...request, + signal: options.timeout !== undefined ? AbortSignal.timeout(options.timeout) : undefined, + }); + }, options.retryOptions); } /** diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index db2ec9f8ceb..57d5610e371 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -33,7 +33,7 @@ describe('Retries correctly', () => { vi.clearAllMocks(); }); - const maxAttempts = 4; + const maxRetries = 4; const duration = 150; function assertBackoff(callTime: number, index: number, arr: number[], expectedWaitTime: number) { @@ -52,37 +52,40 @@ describe('Retries correctly', () => { } test('fixed backoff', async () => { - const retryOptions = { maxAttempts, baseDuration: duration, backoff: 'fixed' as const }; + const retryOptions = { maxRetries, baseDuration: duration, backoff: 'fixed' as const }; const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); const callTimes: number[] = []; - mockFetch(maxAttempts, callTimes); + mockFetch(maxRetries, callTimes); const expectedChainInfo = await provider.operations.getChain(); const chainInfo = await provider.operations.getChain(); expect(chainInfo).toEqual(expectedChainInfo); - expect(callTimes.length - 1).toBe(maxAttempts); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it + expect(callTimes.length - 1).toBe(maxRetries); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it callTimes.forEach((callTime, index) => assertBackoff(callTime, index, callTimes, duration)); }); test('linear backoff', async () => { - const retryOptions = { maxAttempts, baseDuration: duration, backoff: 'linear' as const }; + const retryOptions = { + maxRetries, + backoff: 'linear' as const, + }; const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); const callTimes: number[] = []; - mockFetch(maxAttempts, callTimes); + mockFetch(maxRetries, callTimes); const expectedChainInfo = await provider.operations.getChain(); const chainInfo = await provider.operations.getChain(); expect(chainInfo).toEqual(expectedChainInfo); - expect(callTimes.length - 1).toBe(maxAttempts); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it + expect(callTimes.length - 1).toBe(maxRetries); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it callTimes.forEach((callTime, index) => assertBackoff(callTime, index, callTimes, duration * index) @@ -90,19 +93,22 @@ describe('Retries correctly', () => { }); test('exponential backoff', async () => { - const retryOptions = { maxAttempts, baseDuration: duration, backoff: 'exponential' as const }; + const retryOptions = { + maxRetries, + backoff: 'exponential' as const, + }; const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); const callTimes: number[] = []; - mockFetch(maxAttempts, callTimes); + mockFetch(maxRetries, callTimes); const expectedChainInfo = await provider.operations.getChain(); const chainInfo = await provider.operations.getChain(); expect(chainInfo).toEqual(expectedChainInfo); - expect(callTimes.length - 1).toBe(maxAttempts); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it + expect(callTimes.length - 1).toBe(maxRetries); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it callTimes.forEach((callTime, index) => assertBackoff(callTime, index, callTimes, duration * (2 ^ (index - 1))) @@ -110,7 +116,10 @@ describe('Retries correctly', () => { }); test('throws if last attempt fails', async () => { - const retryOptions = { maxAttempts, baseDuration: duration, backoff: 'fixed' as const }; + const retryOptions = { + maxRetries, + backoff: 'fixed' as const, + }; const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); @@ -127,6 +136,6 @@ describe('Retries correctly', () => { expect(error).toMatchObject({ cause: { code: 'ECONNREFUSED' } }); // the added one is for the initial call which isn't considered a retry attempt - expect(fetchSpy).toHaveBeenCalledTimes(maxAttempts + 1); + expect(fetchSpy).toHaveBeenCalledTimes(maxRetries + 1); }); }); From 70e432c1cace8074db1653e4ce4b95561cfd5513 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 22 Dec 2023 08:19:36 +0100 Subject: [PATCH 42/51] refactor: simplify when no retry --- packages/providers/src/call-retrier.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/providers/src/call-retrier.ts b/packages/providers/src/call-retrier.ts index 41148c4c275..3dd1cedf233 100644 --- a/packages/providers/src/call-retrier.ts +++ b/packages/providers/src/call-retrier.ts @@ -41,11 +41,11 @@ export function retrier( options: RetryOptions | undefined, retryAttempt: number = 0 ): NonNullable { - return async (...args) => { - if (options === undefined) { - return fetchFn(...args); - } + if (options === undefined) { + return fetchFn; + } + return async (...args) => { try { return await fetchFn(...args); } catch (e: unknown) { From 4e59eeeb1ed2416e0a546b92288dcbec9718f0be Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 22 Dec 2023 08:20:47 +0100 Subject: [PATCH 43/51] refactor: spacing --- packages/providers/src/call-retrier.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/providers/src/call-retrier.ts b/packages/providers/src/call-retrier.ts index 3dd1cedf233..40d2f65e21c 100644 --- a/packages/providers/src/call-retrier.ts +++ b/packages/providers/src/call-retrier.ts @@ -63,6 +63,7 @@ export function retrier( retryAttempt += 1; await sleep(getWaitDuration(options, retryAttempt)); + return retrier(fetchFn, options, retryAttempt)(...args); } }; From cfac00bfa198bd9c05f6429dd5c1592ab51598cc Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 22 Dec 2023 08:30:53 +0100 Subject: [PATCH 44/51] fix: test validation --- packages/providers/test/retry.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index 57d5610e371..64f3af35f17 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -28,6 +28,9 @@ function mockFetch(maxAttempts: number, callTimes: number[]) { }); } +/** + * @group node + */ describe('Retries correctly', () => { afterEach(() => { vi.clearAllMocks(); From 08632ce129ab6dcdec0c2b03a9083ade4b804b5b Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 22 Dec 2023 11:23:59 +0100 Subject: [PATCH 45/51] test: fix flaky test --- packages/providers/test/retry.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index 64f3af35f17..2eead5b5b20 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -66,7 +66,7 @@ describe('Retries correctly', () => { const chainInfo = await provider.operations.getChain(); - expect(chainInfo).toEqual(expectedChainInfo); + expect(chainInfo.chain.name).toEqual(expectedChainInfo.chain.name); expect(callTimes.length - 1).toBe(maxRetries); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it callTimes.forEach((callTime, index) => assertBackoff(callTime, index, callTimes, duration)); From e4dcb5e31209d5e9712324a44de009643278d7d4 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 22 Dec 2023 12:56:57 +0100 Subject: [PATCH 46/51] fix: increase expected wait time to reduce flakiness --- packages/providers/test/retry.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index 2eead5b5b20..138dc934699 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -51,7 +51,7 @@ describe('Retries correctly', () => { // this might be something related to the event loop and how it schedules setTimeouts // expectedWaitTime minus 5ms seems like reasonable to allow expect(waitTime).toBeGreaterThanOrEqual(expectedWaitTime - 5); - expect(waitTime).toBeLessThanOrEqual(expectedWaitTime + 10); + expect(waitTime).toBeLessThanOrEqual(expectedWaitTime + 15); } test('fixed backoff', async () => { From f72f8adee32bae820bd50a4b32cd8036b587a3bb Mon Sep 17 00:00:00 2001 From: nedsalk Date: Fri, 22 Dec 2023 13:37:02 +0100 Subject: [PATCH 47/51] docs: mention defaults on RetryOptions --- packages/providers/src/call-retrier.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/providers/src/call-retrier.ts b/packages/providers/src/call-retrier.ts index 40d2f65e21c..0c43e7ea475 100644 --- a/packages/providers/src/call-retrier.ts +++ b/packages/providers/src/call-retrier.ts @@ -9,11 +9,11 @@ export interface RetryOptions { */ maxRetries: number; /** - * Backoff strategy to use when retrying. + * Backoff strategy to use when retrying. Default is exponential. */ backoff?: Backoff; /** - * Base duration for backoff strategy. + * Base duration for backoff strategy. Default is 150ms. */ baseDuration?: number; } From 814659385bd70a4b7104d3310bef883a45a3dab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nedim=20Salki=C4=87?= Date: Wed, 27 Dec 2023 09:38:46 +0100 Subject: [PATCH 48/51] Update packages/providers/src/provider.ts Co-authored-by: Anderson Arboleya --- packages/providers/src/provider.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/providers/src/provider.ts b/packages/providers/src/provider.ts index 11776ca2c07..cf069057944 100644 --- a/packages/providers/src/provider.ts +++ b/packages/providers/src/provider.ts @@ -286,6 +286,8 @@ export default class Provider { }; private static getFetchFn(options: ProviderOptions): NonNullable { + const { retryOptions, timeout } = options; + return retrier((...args) => { if (options.fetch) { return options.fetch(...args); @@ -293,11 +295,10 @@ export default class Provider { const url = args[0]; const request = args[1]; - return fetch(url, { - ...request, - signal: options.timeout !== undefined ? AbortSignal.timeout(options.timeout) : undefined, - }); - }, options.retryOptions); + const signal = timeout ? AbortSignal.timeout(timeout) : undefined; + + return fetch(url, { ...request, signal }); + }, retryOptions); } /** From b783fbe28d762ad92d814b3a2ac93ca38ae43da3 Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 27 Dec 2023 09:56:34 +0100 Subject: [PATCH 49/51] fix: less flaky test --- packages/providers/test/retry.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index 138dc934699..6270a742252 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -87,7 +87,7 @@ describe('Retries correctly', () => { const chainInfo = await provider.operations.getChain(); - expect(chainInfo).toEqual(expectedChainInfo); + expect(chainInfo.chain.name).toEqual(expectedChainInfo.chain.name); expect(callTimes.length - 1).toBe(maxRetries); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it callTimes.forEach((callTime, index) => @@ -110,7 +110,7 @@ describe('Retries correctly', () => { const chainInfo = await provider.operations.getChain(); - expect(chainInfo).toEqual(expectedChainInfo); + expect(chainInfo.chain.name).toEqual(expectedChainInfo.chain.name); expect(callTimes.length - 1).toBe(maxRetries); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it callTimes.forEach((callTime, index) => From 42aa40ef433b7a0eb524063ceb4befc0bd8b497e Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 3 Jan 2024 12:49:18 +0100 Subject: [PATCH 50/51] docs: added for retry options --- apps/docs/.vitepress/config.ts | 4 ++++ .../src/guide/providers/retrying-calls.md | 5 +++++ packages/providers/test/retry.test.ts | 20 ++++++++++++------- 3 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 apps/docs/src/guide/providers/retrying-calls.md diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.ts index 624fa1d5904..cfe47caff1f 100644 --- a/apps/docs/.vitepress/config.ts +++ b/apps/docs/.vitepress/config.ts @@ -240,6 +240,10 @@ export default defineConfig({ text: 'Querying the Chain', link: '/guide/providers/querying-the-chain', }, + { + text: 'Retrying calls', + link: '/guide/providers/retrying-calls', + }, ], }, { diff --git a/apps/docs/src/guide/providers/retrying-calls.md b/apps/docs/src/guide/providers/retrying-calls.md new file mode 100644 index 00000000000..758940c0290 --- /dev/null +++ b/apps/docs/src/guide/providers/retrying-calls.md @@ -0,0 +1,5 @@ +# Retrying calls + +The default behavior of calls done via the `Provider` towards a fuel node is that they'll fail if the connection breaks. Specifying retry options allows you to customize how many additional attempts you want to make when the connection to the node breaks before ultimately throwing an error. You can also specify the backoff algorithm as well as the base duration that algorithm will use to calculate the wait time for each request. + +<<< @/../../../packages/providers/test/retry.test.ts#provider-retry-options{ts:line-numbers} diff --git a/packages/providers/test/retry.test.ts b/packages/providers/test/retry.test.ts index 6270a742252..d6836e4a7aa 100644 --- a/packages/providers/test/retry.test.ts +++ b/packages/providers/test/retry.test.ts @@ -1,5 +1,6 @@ import { safeExec } from '@fuel-ts/errors/test-utils'; +import type { RetryOptions } from '../src/call-retrier'; import Provider from '../src/provider'; // TODO: Figure out a way to import this constant from `@fuel-ts/wallet/configs` @@ -37,7 +38,7 @@ describe('Retries correctly', () => { }); const maxRetries = 4; - const duration = 150; + const baseDuration = 150; function assertBackoff(callTime: number, index: number, arr: number[], expectedWaitTime: number) { if (index === 0) { @@ -55,9 +56,10 @@ describe('Retries correctly', () => { } test('fixed backoff', async () => { - const retryOptions = { maxRetries, baseDuration: duration, backoff: 'fixed' as const }; + const retryOptions: RetryOptions = { maxRetries, baseDuration, backoff: 'fixed' }; const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); + const callTimes: number[] = []; mockFetch(maxRetries, callTimes); @@ -69,7 +71,7 @@ describe('Retries correctly', () => { expect(chainInfo.chain.name).toEqual(expectedChainInfo.chain.name); expect(callTimes.length - 1).toBe(maxRetries); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it - callTimes.forEach((callTime, index) => assertBackoff(callTime, index, callTimes, duration)); + callTimes.forEach((callTime, index) => assertBackoff(callTime, index, callTimes, baseDuration)); }); test('linear backoff', async () => { @@ -91,17 +93,21 @@ describe('Retries correctly', () => { expect(callTimes.length - 1).toBe(maxRetries); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it callTimes.forEach((callTime, index) => - assertBackoff(callTime, index, callTimes, duration * index) + assertBackoff(callTime, index, callTimes, baseDuration * index) ); }); test('exponential backoff', async () => { - const retryOptions = { + // #region provider-retry-options + const retryOptions: RetryOptions = { maxRetries, - backoff: 'exponential' as const, + baseDuration, + backoff: 'exponential', }; const provider = await Provider.create(FUEL_NETWORK_URL, { retryOptions }); + // #endregion provider-retry-options + const callTimes: number[] = []; mockFetch(maxRetries, callTimes); @@ -114,7 +120,7 @@ describe('Retries correctly', () => { expect(callTimes.length - 1).toBe(maxRetries); // callTimes.length - 1 is for the initial call that's not a retry so we ignore it callTimes.forEach((callTime, index) => - assertBackoff(callTime, index, callTimes, duration * (2 ^ (index - 1))) + assertBackoff(callTime, index, callTimes, baseDuration * (2 ^ (index - 1))) ); }); From a7258a7ec4743a58549df7c96a04e9525e2b406c Mon Sep 17 00:00:00 2001 From: nedsalk Date: Wed, 3 Jan 2024 12:50:33 +0100 Subject: [PATCH 51/51] fix: spellcheck --- apps/docs/src/guide/providers/retrying-calls.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/src/guide/providers/retrying-calls.md b/apps/docs/src/guide/providers/retrying-calls.md index 758940c0290..e5f3ac76593 100644 --- a/apps/docs/src/guide/providers/retrying-calls.md +++ b/apps/docs/src/guide/providers/retrying-calls.md @@ -1,5 +1,5 @@ # Retrying calls -The default behavior of calls done via the `Provider` towards a fuel node is that they'll fail if the connection breaks. Specifying retry options allows you to customize how many additional attempts you want to make when the connection to the node breaks before ultimately throwing an error. You can also specify the backoff algorithm as well as the base duration that algorithm will use to calculate the wait time for each request. +The default behavior of calls done via the `Provider` towards a fuel node is that they'll fail if the connection breaks. Specifying retry options allows you to customize how many additional attempts you want to make when the connection to the node breaks before ultimately throwing an error. You can also specify the back-off algorithm as well as the base duration that algorithm will use to calculate the wait time for each request. <<< @/../../../packages/providers/test/retry.test.ts#provider-retry-options{ts:line-numbers}