diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc41338..5ef2b36 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,47 @@ 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 rxjs@^7.3.0 + + - run: yarn lint + + - 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 diff --git a/__tests__/timeoutLink.test.ts b/__tests__/timeoutLink.test.ts index f6907b7..eda0fa7 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,18 @@ const mockLink = new ApolloLink(() => { const link = timeoutLink.concat(mockLink); +const client = new ApolloClient({ + link: link, + cache: new InMemoryCache(), +}); +const executeWrapper = (link: ApolloLink, operation: GraphQLRequest) => { + return execute.length === 3 + // @ts-ignore + ? execute(link, operation, { client }) + // @ts-ignore + : execute(link, operation); +}; + beforeEach(() => { called = 0; }); @@ -37,7 +49,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 +64,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 +82,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 +98,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 +123,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 +146,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 +168,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 +185,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 +200,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 +234,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 +246,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 +269,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..ee14ff6 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; /** @@ -56,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) => { let timer: any; // listen to chainObservable for result and pass to localObservable if received before timeout const subscription = chainObservable.subscribe( - result => { + (result) => { clearTimeout(timer); observer.next(result); observer.complete(); }, - error => { + (error) => { clearTimeout(timer); observer.error(error); observer.complete(); 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"],