From 7a9948f33d09ada06551caa84efce77cdf689ce7 Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Thu, 25 Dec 2025 17:11:47 -0700 Subject: [PATCH 01/11] feat: Support @apollo/client@^4 Signed-off-by: Matthew Peveler --- .github/workflows/ci.yml | 28 ++++++++++++++++-- __tests__/timeoutLink.test.ts | 55 +++++++++++++++++++++++++---------- src/timeoutLink.ts | 6 +++- 3 files changed, 70 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc41338..d40722b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,12 +9,12 @@ on: - master jobs: - test: + test-apollo3: runs-on: ubuntu-latest strategy: matrix: - node-version: [12.x, 14.x, 16.x, 20.x, 22.x, 23.x] + node-version: [12.x, 14.x, 16.x, 20.x, 22.x, 24.x, 25.x] steps: - uses: actions/checkout@v4 @@ -31,3 +31,27 @@ jobs: - run: yarn build - run: yarn test + + test-apollo4: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x, 20.x, 22.x, 24.x, 25.x] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - run: yarn --ignore-scripts + - run: yarn add --ignore-scripts --dev @apollo/client@4.0.0 graphql@16.0.0 + + - run: yarn lint + + - run: yarn build + + - run: yarn test diff --git a/__tests__/timeoutLink.test.ts b/__tests__/timeoutLink.test.ts index f6907b7..5d3b5a1 100644 --- a/__tests__/timeoutLink.test.ts +++ b/__tests__/timeoutLink.test.ts @@ -1,5 +1,5 @@ import TimeoutLink from '../src/timeoutLink'; -import { ApolloLink, execute, Observable, HttpLink } from '@apollo/client/core'; +import { ApolloClient, ApolloLink, execute, type GraphQLRequest, HttpLink, InMemoryCache, Observable, } from '@apollo/client/core'; import gql from 'graphql-tag'; const TEST_TIMEOUT = 100; @@ -30,6 +30,28 @@ const mockLink = new ApolloLink(() => { const link = timeoutLink.concat(mockLink); +const client = new ApolloClient({ + link, + cache: new InMemoryCache(), +}); + +let apolloClientInstance: ApolloClient | null = null; +const executeWrapper = (link: ApolloLink, operation: GraphQLRequest) => { + if (execute.length === 3 && !apolloClientInstance) { + const httpLink = new HttpLink({ uri: "/graphql" }); + const timeoutHttpLink = timeoutLink.concat(httpLink); + apolloClientInstance = new ApolloClient({ + link: timeoutHttpLink, + cache: new InMemoryCache(), + }); + } + return execute.length === 3 + // @ts-ignore + ? execute(link, operation, { client: apolloClientInstance }) + // @ts-ignore + : execute(link, operation); +}; + beforeEach(() => { called = 0; }); @@ -37,7 +59,7 @@ beforeEach(() => { test('short request does not timeout', done => { delay = 50; - execute(link, { query }).subscribe({ + executeWrapper(link, { query }).subscribe({ next() { expect(called).toBe(1); done(); @@ -52,7 +74,7 @@ test('short request does not timeout', done => { test('long request times out', done => { delay = 200; - execute(link, { query }).subscribe({ + executeWrapper(link, { query }).subscribe({ next() { expect('next called').toBeFalsy(); done(); @@ -70,7 +92,7 @@ test('configured value through context does not time out', done => { delay = 200; const configured = 500; - execute(link, { query, context: { timeout: configured } }).subscribe({ + executeWrapper(link, { query, context: { timeout: configured } }).subscribe({ next() { expect(called).toBe(1); done(); @@ -86,7 +108,7 @@ test('configured short value through context time out', done => { delay = 200; const configured = 100; - execute(link, { query, context: { timeout: configured } }).subscribe({ + executeWrapper(link, { query, context: { timeout: configured } }).subscribe({ next() { expect('next called').toBeFalsy(); done(); @@ -111,7 +133,7 @@ test('configured value through prior link does not time out', done => { const testLink = configLink.concat(timeoutLink).concat(mockLink); - execute(testLink, { query }).subscribe({ + executeWrapper(testLink, { query }).subscribe({ next() { expect(called).toBe(1); done(); @@ -134,7 +156,7 @@ test('configured short value through prior link time out', done => { const testLink = configLink.concat(timeoutLink).concat(mockLink); - execute(testLink, { query }).subscribe({ + executeWrapper(testLink, { query }).subscribe({ next() { expect('next called').toBeFalsy(); done(); @@ -156,7 +178,7 @@ test('aborted request does not timeout', done => { controller.abort(); - execute(link, { query, context: { fetchOptions } }).subscribe({ + executeWrapper(link, { query, context: { fetchOptions } }).subscribe({ next() { expect(called).toBe(1); done(); @@ -173,7 +195,7 @@ test('negative timeout via constructor disables timeout', done => { const testLink = new TimeoutLink(-1).concat(mockLink); - execute(testLink, { query }).subscribe({ + executeWrapper(testLink, { query }).subscribe({ next() { expect(called).toBe(1); done(); @@ -188,7 +210,7 @@ test('negative timeout via constructor disables timeout', done => { test('negative timeout via context disables timeout', done => { delay = 150; - execute(link, { query, context: { timeout: -1 } }).subscribe({ + executeWrapper(link, { query, context: { timeout: -1 } }).subscribe({ next() { expect(called).toBe(1); done(); @@ -222,6 +244,7 @@ if (nodeMajor >= 20) { const terminalLink = timeoutLink.concat( new HttpLink({ uri: "https://example.com/graphql", + // @ts-ignore fetch: (_, { signal }) => { finalSignal = signal; return Promise.resolve( @@ -233,22 +256,22 @@ if (nodeMajor >= 20) { }, }) ); - + let events: Array< | { type: "next"; result: unknown } | { type: "done" } | { type: "error"; error: unknown } > = []; - const subsciption = execute(terminalLink, { + const subsciption = executeWrapper(terminalLink, { query: subscription, }).subscribe({ next: (result) => events.push({ type: "next", result }), error: (error) => events.push({ type: "error", error }), complete: () => events.push({ type: "done" }), }); - + const writer = stream.writable.getWriter(); - + writer.write(` --- Content-Type: application/json @@ -256,13 +279,13 @@ Content-Type: application/json {"data":{"chunk": "first"}} --- `.trim()); - + setTimeout(() => { expect(events).toStrictEqual([ { type: "next", result: { data: { chunk: "first" } } }, ]); subsciption.unsubscribe(); - + setTimeout(() => { expect(finalSignal?.aborted).toBeTruthy(); done(); diff --git a/src/timeoutLink.ts b/src/timeoutLink.ts index 449fc6a..d2321e9 100644 --- a/src/timeoutLink.ts +++ b/src/timeoutLink.ts @@ -1,8 +1,12 @@ // note, this import is modified when building for ESM via `script/fix_apollo_import.mjs` -import { ApolloLink, Observable, type Operation, type NextLink, type FetchResult } from '@apollo/client/core'; +import { ApolloLink, Observable, type Operation, type FetchResult } from '@apollo/client/core'; import type { DefinitionNode } from 'graphql'; import TimeoutError from './TimeoutError.js'; +// NextLink was removed from apollo-client in v4 and replaced by ApolloLink.ForwardFunction, +// but to maintain compatibility with both v3 and v4, we re-declare it here. +type NextLink = (operation: Operation) => Observable; + const DEFAULT_TIMEOUT: number = 15000; /** From 8d2db3571e4c98da4b92bcf4213fd014bcd68582 Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Thu, 25 Dec 2025 17:13:49 -0700 Subject: [PATCH 02/11] bump tsconfig target Signed-off-by: Matthew Peveler --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 92bf349..e311b09 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2016", "module": "commonjs", "moduleResolution": "node", "lib": ["es2015", "dom"], From 25cd63746ef29146cc1e0e1de007e7cc42819f0b Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Thu, 25 Dec 2025 17:16:28 -0700 Subject: [PATCH 03/11] fix test type error Signed-off-by: Matthew Peveler --- __tests__/timeoutLink.test.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/__tests__/timeoutLink.test.ts b/__tests__/timeoutLink.test.ts index 5d3b5a1..eda0fa7 100644 --- a/__tests__/timeoutLink.test.ts +++ b/__tests__/timeoutLink.test.ts @@ -31,23 +31,13 @@ const mockLink = new ApolloLink(() => { const link = timeoutLink.concat(mockLink); const client = new ApolloClient({ - link, + link: link, cache: new InMemoryCache(), }); - -let apolloClientInstance: ApolloClient | null = null; const executeWrapper = (link: ApolloLink, operation: GraphQLRequest) => { - if (execute.length === 3 && !apolloClientInstance) { - const httpLink = new HttpLink({ uri: "/graphql" }); - const timeoutHttpLink = timeoutLink.concat(httpLink); - apolloClientInstance = new ApolloClient({ - link: timeoutHttpLink, - cache: new InMemoryCache(), - }); - } return execute.length === 3 // @ts-ignore - ? execute(link, operation, { client: apolloClientInstance }) + ? execute(link, operation, { client }) // @ts-ignore : execute(link, operation); }; From d3c98a55a573f21362884b3d46ca95a1a1f24c0b Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Thu, 25 Dec 2025 17:24:24 -0700 Subject: [PATCH 04/11] add types Signed-off-by: Matthew Peveler --- src/timeoutLink.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/timeoutLink.ts b/src/timeoutLink.ts index d2321e9..fa49998 100644 --- a/src/timeoutLink.ts +++ b/src/timeoutLink.ts @@ -1,5 +1,5 @@ // note, this import is modified when building for ESM via `script/fix_apollo_import.mjs` -import { ApolloLink, Observable, type Operation, type FetchResult } from '@apollo/client/core'; +import { ApolloLink, Observable, type Operation, type FetchResult, type Observer } from '@apollo/client/core'; import type { DefinitionNode } from 'graphql'; import TimeoutError from './TimeoutError.js'; @@ -60,17 +60,17 @@ export default class TimeoutLink extends ApolloLink { // create local observable with timeout functionality (unsubscibe from chain observable and // return an error if the timeout expires before chain observable resolves) - const localObservable = new Observable(observer => { + const localObservable = new Observable((observer: Observer) => { let timer: any; // listen to chainObservable for result and pass to localObservable if received before timeout const subscription = chainObservable.subscribe( - result => { + (result: FetchResult) => { clearTimeout(timer); observer.next(result); observer.complete(); }, - error => { + (error: Error) => { clearTimeout(timer); observer.error(error); observer.complete(); From 533365b5407c397f829d9fbadeafd306a1bbf593 Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Thu, 25 Dec 2025 17:28:20 -0700 Subject: [PATCH 05/11] shim another type --- src/timeoutLink.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/timeoutLink.ts b/src/timeoutLink.ts index fa49998..e141560 100644 --- a/src/timeoutLink.ts +++ b/src/timeoutLink.ts @@ -1,5 +1,5 @@ // note, this import is modified when building for ESM via `script/fix_apollo_import.mjs` -import { ApolloLink, Observable, type Operation, type FetchResult, type Observer } from '@apollo/client/core'; +import { ApolloLink, Observable, type Operation, type FetchResult } from '@apollo/client/core'; import type { DefinitionNode } from 'graphql'; import TimeoutError from './TimeoutError.js'; @@ -9,6 +9,12 @@ type NextLink = (operation: Operation) => Observable; const DEFAULT_TIMEOUT: number = 15000; +type Observer = { + next?: (value: T) => void; + error?: (error: any) => void; + complete?: () => void; +} + /** * Aborts the request if the timeout expires before the response is received. */ From 65d022559b9676b163695753fc2c56a9d6b60c80 Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Thu, 25 Dec 2025 17:32:34 -0700 Subject: [PATCH 06/11] try to fix more type errors with v4 --- __tests__/timeoutLink.test.ts | 21 ++++++++++++++------- src/timeoutLink.ts | 6 +++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/__tests__/timeoutLink.test.ts b/__tests__/timeoutLink.test.ts index eda0fa7..350b772 100644 --- a/__tests__/timeoutLink.test.ts +++ b/__tests__/timeoutLink.test.ts @@ -1,6 +1,7 @@ import TimeoutLink from '../src/timeoutLink'; -import { ApolloClient, ApolloLink, execute, type GraphQLRequest, HttpLink, InMemoryCache, Observable, } from '@apollo/client/core'; +import { ApolloClient, ApolloLink, execute, FetchResult, type GraphQLRequest, HttpLink, InMemoryCache, Observable, } from '@apollo/client/core'; import gql from 'graphql-tag'; +import TimeoutError from '../src/TimeoutError'; const TEST_TIMEOUT = 100; @@ -18,9 +19,15 @@ const query = gql` let called: number; let delay: number; +type Observer = { + next: (value: T) => void; + error: (error: any) => void; + complete: () => void; +} + const mockLink = new ApolloLink(() => { called++; - return new Observable(observer => { + return new Observable((observer: Observer) => { setTimeout(() => { observer.next({}); observer.complete(); @@ -69,7 +76,7 @@ test('long request times out', done => { expect('next called').toBeFalsy(); done(); }, - error(error) { + error(error: TimeoutError) { expect(error.message).toEqual('Timeout exceeded'); expect(error.timeout).toEqual(100); expect(error.statusCode).toEqual(408); @@ -103,7 +110,7 @@ test('configured short value through context time out', done => { expect('next called').toBeFalsy(); done(); }, - error(error) { + error(error: TimeoutError) { expect(error.message).toEqual('Timeout exceeded'); expect(error.timeout).toEqual(configured); expect(error.statusCode).toEqual(408); @@ -151,7 +158,7 @@ test('configured short value through prior link time out', done => { expect('next called').toBeFalsy(); done(); }, - error(error) { + error(error: TimeoutError) { expect(error.message).toEqual('Timeout exceeded'); expect(error.timeout).toEqual(configured); expect(error.statusCode).toEqual(408); @@ -255,8 +262,8 @@ if (nodeMajor >= 20) { const subsciption = executeWrapper(terminalLink, { query: subscription, }).subscribe({ - next: (result) => events.push({ type: "next", result }), - error: (error) => events.push({ type: "error", error }), + next: (result: FetchResult) => events.push({ type: "next", result }), + error: (error: TimeoutError ) => events.push({ type: "error", error }), complete: () => events.push({ type: "done" }), }); diff --git a/src/timeoutLink.ts b/src/timeoutLink.ts index e141560..fe605ac 100644 --- a/src/timeoutLink.ts +++ b/src/timeoutLink.ts @@ -10,9 +10,9 @@ type NextLink = (operation: Operation) => Observable; const DEFAULT_TIMEOUT: number = 15000; type Observer = { - next?: (value: T) => void; - error?: (error: any) => void; - complete?: () => void; + next: (value: T) => void; + error: (error: any) => void; + complete: () => void; } /** From d3feaeb271f3ef4c683e08fc6db091450966dea4 Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Thu, 25 Dec 2025 17:35:05 -0700 Subject: [PATCH 07/11] add yarn add rxjs to ci --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d40722b..61083fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: node-version: ${{ matrix.node-version }} - run: yarn --ignore-scripts - - run: yarn add --ignore-scripts --dev @apollo/client@4.0.0 graphql@16.0.0 + - run: yarn add --ignore-scripts --dev @apollo/client@4.0.0 graphql@16.0.0 rxjs@7.3.0 - run: yarn lint From 81999c08132e01106d6511144e36b7234eb045e9 Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Thu, 25 Dec 2025 17:36:43 -0700 Subject: [PATCH 08/11] remove type --- __tests__/timeoutLink.test.ts | 21 +++++++-------------- src/timeoutLink.ts | 8 +------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/__tests__/timeoutLink.test.ts b/__tests__/timeoutLink.test.ts index 350b772..eda0fa7 100644 --- a/__tests__/timeoutLink.test.ts +++ b/__tests__/timeoutLink.test.ts @@ -1,7 +1,6 @@ import TimeoutLink from '../src/timeoutLink'; -import { ApolloClient, ApolloLink, execute, FetchResult, type GraphQLRequest, HttpLink, InMemoryCache, Observable, } from '@apollo/client/core'; +import { ApolloClient, ApolloLink, execute, type GraphQLRequest, HttpLink, InMemoryCache, Observable, } from '@apollo/client/core'; import gql from 'graphql-tag'; -import TimeoutError from '../src/TimeoutError'; const TEST_TIMEOUT = 100; @@ -19,15 +18,9 @@ const query = gql` let called: number; let delay: number; -type Observer = { - next: (value: T) => void; - error: (error: any) => void; - complete: () => void; -} - const mockLink = new ApolloLink(() => { called++; - return new Observable((observer: Observer) => { + return new Observable(observer => { setTimeout(() => { observer.next({}); observer.complete(); @@ -76,7 +69,7 @@ test('long request times out', done => { expect('next called').toBeFalsy(); done(); }, - error(error: TimeoutError) { + error(error) { expect(error.message).toEqual('Timeout exceeded'); expect(error.timeout).toEqual(100); expect(error.statusCode).toEqual(408); @@ -110,7 +103,7 @@ test('configured short value through context time out', done => { expect('next called').toBeFalsy(); done(); }, - error(error: TimeoutError) { + error(error) { expect(error.message).toEqual('Timeout exceeded'); expect(error.timeout).toEqual(configured); expect(error.statusCode).toEqual(408); @@ -158,7 +151,7 @@ test('configured short value through prior link time out', done => { expect('next called').toBeFalsy(); done(); }, - error(error: TimeoutError) { + error(error) { expect(error.message).toEqual('Timeout exceeded'); expect(error.timeout).toEqual(configured); expect(error.statusCode).toEqual(408); @@ -262,8 +255,8 @@ if (nodeMajor >= 20) { const subsciption = executeWrapper(terminalLink, { query: subscription, }).subscribe({ - next: (result: FetchResult) => events.push({ type: "next", result }), - error: (error: TimeoutError ) => events.push({ type: "error", error }), + next: (result) => events.push({ type: "next", result }), + error: (error) => events.push({ type: "error", error }), complete: () => events.push({ type: "done" }), }); diff --git a/src/timeoutLink.ts b/src/timeoutLink.ts index fe605ac..c8c037a 100644 --- a/src/timeoutLink.ts +++ b/src/timeoutLink.ts @@ -9,12 +9,6 @@ type NextLink = (operation: Operation) => Observable; const DEFAULT_TIMEOUT: number = 15000; -type Observer = { - next: (value: T) => void; - error: (error: any) => void; - complete: () => void; -} - /** * Aborts the request if the timeout expires before the response is received. */ @@ -66,7 +60,7 @@ export default class TimeoutLink extends ApolloLink { // create local observable with timeout functionality (unsubscibe from chain observable and // return an error if the timeout expires before chain observable resolves) - const localObservable = new Observable((observer: Observer) => { + const localObservable = new Observable((observer) => { let timer: any; // listen to chainObservable for result and pass to localObservable if received before timeout From 40ea3a2c0964f43c3fbfe04de676f699ed22050c Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Thu, 25 Dec 2025 17:41:21 -0700 Subject: [PATCH 09/11] try installing ^7.3.0 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61083fa..9af13da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: node-version: ${{ matrix.node-version }} - run: yarn --ignore-scripts - - run: yarn add --ignore-scripts --dev @apollo/client@4.0.0 graphql@16.0.0 rxjs@7.3.0 + - run: yarn add --ignore-scripts --dev @apollo/client@4.0.0 graphql@16.0.0 rxjs@^7.3.0 - run: yarn lint From a1c4fb3cbba1d1f215a8176c329e8a310ac8d06d Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Thu, 25 Dec 2025 17:43:28 -0700 Subject: [PATCH 10/11] remove other types --- src/timeoutLink.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/timeoutLink.ts b/src/timeoutLink.ts index c8c037a..ee14ff6 100644 --- a/src/timeoutLink.ts +++ b/src/timeoutLink.ts @@ -65,12 +65,12 @@ export default class TimeoutLink extends ApolloLink { // listen to chainObservable for result and pass to localObservable if received before timeout const subscription = chainObservable.subscribe( - (result: FetchResult) => { + (result) => { clearTimeout(timer); observer.next(result); observer.complete(); }, - (error: Error) => { + (error) => { clearTimeout(timer); observer.error(error); observer.complete(); From 4e7dbfeb8010a8f0ac56f6669f5c15705bcdd771 Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Thu, 25 Dec 2025 17:48:25 -0700 Subject: [PATCH 11/11] test on latest peer dependencies Signed-off-by: Matthew Peveler --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9af13da..5ef2b36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,3 +55,23 @@ jobs: - run: yarn build - run: yarn test + + test-apollo4-latest: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24.x + + - run: yarn --ignore-scripts + - run: yarn add --ignore-scripts --dev @apollo/client graphql rxjs + + - run: yarn lint + + - run: yarn build + + - run: yarn test