diff --git a/.changeset/@graphql-mesh_cache-redis-7401-dependencies.md b/.changeset/@graphql-mesh_cache-redis-7401-dependencies.md new file mode 100644 index 0000000000000..270e513541394 --- /dev/null +++ b/.changeset/@graphql-mesh_cache-redis-7401-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/cache-redis": patch +--- +dependencies updates: + - Added dependency [`@whatwg-node/disposablestack@^0.0.1` ↗︎](https://www.npmjs.com/package/@whatwg-node/disposablestack/v/0.0.1) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_fusion-runtime-7401-dependencies.md b/.changeset/@graphql-mesh_fusion-runtime-7401-dependencies.md new file mode 100644 index 0000000000000..9dff2246bb23a --- /dev/null +++ b/.changeset/@graphql-mesh_fusion-runtime-7401-dependencies.md @@ -0,0 +1,6 @@ +--- +"@graphql-mesh/fusion-runtime": patch +--- +dependencies updates: + - Added dependency [`@whatwg-node/disposablestack@^0.0.1` ↗︎](https://www.npmjs.com/package/@whatwg-node/disposablestack/v/0.0.1) (to `dependencies`) + - Removed dependency [`disposablestack@^1.1.6` ↗︎](https://www.npmjs.com/package/disposablestack/v/1.1.6) (from `dependencies`) diff --git a/.changeset/@graphql-mesh_plugin-hive-7401-dependencies.md b/.changeset/@graphql-mesh_plugin-hive-7401-dependencies.md new file mode 100644 index 0000000000000..3b1cbf4c84969 --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-hive-7401-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/plugin-hive": patch +--- +dependencies updates: + - Added dependency [`@graphql-mesh/utils@^0.99.4` ↗︎](https://www.npmjs.com/package/@graphql-mesh/utils/v/0.99.4) (to `peerDependencies`) diff --git a/.changeset/@graphql-mesh_serve-runtime-7401-dependencies.md b/.changeset/@graphql-mesh_serve-runtime-7401-dependencies.md new file mode 100644 index 0000000000000..d84100147effc --- /dev/null +++ b/.changeset/@graphql-mesh_serve-runtime-7401-dependencies.md @@ -0,0 +1,6 @@ +--- +"@graphql-mesh/serve-runtime": patch +--- +dependencies updates: + - Added dependency [`@whatwg-node/disposablestack@^0.0.1` ↗︎](https://www.npmjs.com/package/@whatwg-node/disposablestack/v/0.0.1) (to `dependencies`) + - Removed dependency [`disposablestack@^1.1.6` ↗︎](https://www.npmjs.com/package/disposablestack/v/1.1.6) (from `dependencies`) diff --git a/.changeset/@graphql-mesh_transport-http-callback-7401-dependencies.md b/.changeset/@graphql-mesh_transport-http-callback-7401-dependencies.md new file mode 100644 index 0000000000000..89d70087522f3 --- /dev/null +++ b/.changeset/@graphql-mesh_transport-http-callback-7401-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/transport-http-callback": patch +--- +dependencies updates: + - Added dependency [`@graphql-mesh/utils@^0.99.4` ↗︎](https://www.npmjs.com/package/@graphql-mesh/utils/v/0.99.4) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_transport-ws-7401-dependencies.md b/.changeset/@graphql-mesh_transport-ws-7401-dependencies.md new file mode 100644 index 0000000000000..81fa2cfe04650 --- /dev/null +++ b/.changeset/@graphql-mesh_transport-ws-7401-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-mesh/transport-ws": patch +--- +dependencies updates: + - Added dependency [`@graphql-mesh/utils@^0.99.4` ↗︎](https://www.npmjs.com/package/@graphql-mesh/utils/v/0.99.4) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_utils-7401-dependencies.md b/.changeset/@graphql-mesh_utils-7401-dependencies.md new file mode 100644 index 0000000000000..0e9829d0b5121 --- /dev/null +++ b/.changeset/@graphql-mesh_utils-7401-dependencies.md @@ -0,0 +1,6 @@ +--- +"@graphql-mesh/utils": patch +--- +dependencies updates: + - Added dependency [`@whatwg-node/disposablestack@^0.0.1` ↗︎](https://www.npmjs.com/package/@whatwg-node/disposablestack/v/0.0.1) (to `dependencies`) + - Removed dependency [`disposablestack@^1.1.6` ↗︎](https://www.npmjs.com/package/disposablestack/v/1.1.6) (from `dependencies`) diff --git a/.husky/pre-commit b/.husky/pre-commit index d2ae35e84b09c..372362317175c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - yarn lint-staged diff --git a/declarations.d.ts b/declarations.d.ts index dc20253c416d5..0aa1b0505ef92 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -18,9 +18,4 @@ declare module '@newrelic/test-utilities' { export const TestAgent: any; } -declare module 'disposablestack/AsyncDisposableStack' { - declare var AsyncDisposableStackCtor: typeof AsyncDisposableStack; - export = AsyncDisposableStackCtor; -} - declare var __VERSION__: string; diff --git a/e2e/federation-subscriptions-passthrough/federation-subscriptions-passthrough.test.ts b/e2e/federation-subscriptions-passthrough/federation-subscriptions-passthrough.test.ts index a490630f723ca..588dd7a892fba 100644 --- a/e2e/federation-subscriptions-passthrough/federation-subscriptions-passthrough.test.ts +++ b/e2e/federation-subscriptions-passthrough/federation-subscriptions-passthrough.test.ts @@ -1,8 +1,6 @@ -import { createServer } from 'http'; -import type { AddressInfo } from 'net'; import { setTimeout } from 'timers/promises'; import { createClient, type Client } from 'graphql-sse'; -import { createTenv } from '@e2e/tenv'; +import { createTenv, getAvailablePort } from '@e2e/tenv'; import { TOKEN } from './services/products/server'; const { composeWithApollo, service, serve } = createTenv(__dirname); @@ -142,10 +140,7 @@ it('should subscribe and resolve via http callbacks', async () => { ]); // Get a random available port - const dummyServer = createServer(); - await new Promise(resolve => dummyServer.listen(0, resolve)); - const availablePort = (dummyServer.address() as AddressInfo).port; - await new Promise(resolve => dummyServer.close(resolve)); + const availablePort = await getAvailablePort(); const publicUrl = `http://0.0.0.0:${availablePort}`; await serve({ diff --git a/e2e/utils/leftoverStack.ts b/e2e/utils/leftoverStack.ts index e791f7a2e9570..b7711669c2947 100644 --- a/e2e/utils/leftoverStack.ts +++ b/e2e/utils/leftoverStack.ts @@ -1,12 +1,9 @@ -import AsyncDisposableStack from 'disposablestack/AsyncDisposableStack'; +import { AsyncDisposableStack } from '@whatwg-node/disposablestack'; -let leftoverStack = new AsyncDisposableStack(); +export let leftoverStack = new AsyncDisposableStack(); -export function getLeftoverStack() { - if (leftoverStack.disposed) { +afterAll(() => + leftoverStack.disposeAsync().finally(() => { leftoverStack = new AsyncDisposableStack(); - } - return leftoverStack; -} - -afterAll(() => leftoverStack.disposeAsync()); + }), +); diff --git a/e2e/utils/tbench.ts b/e2e/utils/tbench.ts index a2160dd0960fc..acdf878894fcb 100644 --- a/e2e/utils/tbench.ts +++ b/e2e/utils/tbench.ts @@ -1,6 +1,6 @@ import { setTimeout } from 'timers/promises'; import { spawn, Thread, Worker } from 'threads'; -import { getLeftoverStack } from './leftoverStack'; +import { leftoverStack } from './leftoverStack'; import { timeout as jestTimeout, type Server } from './tenv'; import type { benchGraphQLServer } from './workers/benchGraphQLServer'; @@ -51,7 +51,7 @@ export async function createTbench(vusCount: number): Promise { .map(() => spawn(new Worker('./workers/benchGraphQLServer.js'))), ); vus.forEach(worker => { - getLeftoverStack().defer(() => Thread.terminate(worker)); + leftoverStack.defer(() => Thread.terminate(worker)); }); return { async sustain({ server, duration = jestTimeout - 10_000, parallelRequestsPerVU = 10, params }) { diff --git a/e2e/utils/tenv.ts b/e2e/utils/tenv.ts index fb05e627ed74e..3fa15f69b9194 100644 --- a/e2e/utils/tenv.ts +++ b/e2e/utils/tenv.ts @@ -13,8 +13,9 @@ import { RemoteGraphQLDataSource, type ServiceEndpointDefinition, } from '@apollo/gateway'; +import { DisposableSymbols } from '@whatwg-node/disposablestack'; import { createArg, createPortArg, createServicePortArg } from './args'; -import { getLeftoverStack } from './leftoverStack'; +import { leftoverStack } from './leftoverStack'; export const retries = 120, interval = 500, @@ -210,7 +211,7 @@ export function createTenv(cwd: string): Tenv { }, async tempfile(name) { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'graphql-mesh_e2e_fs')); - getLeftoverStack().defer(() => fs.rm(tempDir, { recursive: true })); + leftoverStack.defer(() => fs.rm(tempDir, { recursive: true })); return path.join(tempDir, name); }, write(filePath, content) { @@ -340,7 +341,7 @@ export function createTenv(cwd: string): Tenv { let output = ''; if (opts?.output) { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'graphql-mesh_e2e_compose')); - getLeftoverStack().defer(() => fs.rm(tempDir, { recursive: true })); + leftoverStack.defer(() => fs.rm(tempDir, { recursive: true })); output = path.join(tempDir, `${Math.random().toString(32).slice(2)}.${opts.output}`); } const [proc, waitForExit] = await spawn( @@ -501,7 +502,7 @@ export function createTenv(cwd: string): Tenv { getStats() { throw new Error('Cannot get stats of a container.'); }, - async [Symbol.asyncDispose]() { + async [DisposableSymbols.asyncDispose]() { if (ctrl.signal.aborted) { // noop if already disposed return; @@ -510,7 +511,7 @@ export function createTenv(cwd: string): Tenv { await ctr.stop({ t: 0 }); }, }; - getLeftoverStack().use(container); + leftoverStack.use(container); // verify that the container has started await setTimeout(interval); @@ -540,13 +541,13 @@ export function createTenv(cwd: string): Tenv { } if (status === 'none') { - await container[Symbol.asyncDispose](); + await container[DisposableSymbols.asyncDispose](); throw new DockerError( 'Container has "none" health status, but has a healthcheck', container, ); } else if (status === 'unhealthy') { - await container[Symbol.asyncDispose](); + await container[DisposableSymbols.asyncDispose](); throw new DockerError('Container is unhealthy', container); } else if (status === 'healthy') { break; @@ -652,9 +653,9 @@ function spawn( mem: parseFloat(mem) * 0.001, // KB to MB }; }, - [Symbol.asyncDispose]: () => (child.kill(), waitForExit), + [DisposableSymbols.asyncDispose]: () => (child.kill(), waitForExit), }; - getLeftoverStack().use(proc); + leftoverStack.use(proc); child.stdout.on('data', x => { stdout += x.toString(); diff --git a/examples/federation-example/tests/polling.test.ts b/examples/federation-example/tests/polling.test.ts index 492968a293dc9..9e75e9c52029f 100644 --- a/examples/federation-example/tests/polling.test.ts +++ b/examples/federation-example/tests/polling.test.ts @@ -1,7 +1,8 @@ -import { exec, execSync } from 'child_process'; -import { createReadStream, readFile, readFileSync, write, writeFileSync } from 'fs'; -import { createServer } from 'http'; +import { exec } from 'child_process'; +import { readFileSync } from 'fs'; import { join } from 'path'; +import { createDisposableServer } from '../../../packages/testing/createDisposableServer'; +import { disposableExec } from '../../../packages/testing/disposableExec'; jest.setTimeout(30000); async function findAvailableHostName() { @@ -25,16 +26,12 @@ async function findAvailableHostName() { throw new Error('No available hostname found'); } describe('Polling Test', () => { - let cleanupCallbacks: (() => void)[] = []; - afterAll(() => { - cleanupCallbacks.forEach(cb => cb()); - }); it('should pass', async () => { const cwd = join(__dirname, 'fixtures/polling'); const supergraphSdlPath = join(cwd, 'supergraph.graphql'); const supergraphSdl = readFileSync(supergraphSdlPath, 'utf-8'); let changedSupergraph = false; - const supergraphSdlServer = createServer((req, res) => { + await using supergraphSdlServer = await createDisposableServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); console.log('Serving supergraph SDL'); @@ -44,8 +41,6 @@ describe('Polling Test', () => { res.end(supergraphSdl); } }); - await new Promise(resolve => supergraphSdlServer.listen(0, resolve)); - cleanupCallbacks.push(() => supergraphSdlServer.close()); const SUPERGRAPH_SOURCE = `http://localhost:${(supergraphSdlServer.address() as any).port}`; console.info('Supergraph SDL server is running on ' + SUPERGRAPH_SOURCE); const buildCmd = exec(`${join(__dirname, '../node_modules/.bin/mesh')} build`, { @@ -63,14 +58,13 @@ describe('Polling Test', () => { } }); }); - const serveCmd = exec(`${join(__dirname, '../node_modules/.bin/mesh')} start`, { + using serveCmd = disposableExec(`${join(__dirname, '../node_modules/.bin/mesh')} start`, { cwd, env: { ...process.env, SUPERGRAPH_SOURCE, }, }); - cleanupCallbacks.push(() => serveCmd.kill()); await new Promise(resolve => { serveCmd.stderr?.on('data', function stderrListener(data: string) { console.log(data); diff --git a/examples/json-schema-example/tests/artifacts.test.ts b/examples/json-schema-example/tests/artifacts.test.ts index d5e510b868c97..c3ac8de603b79 100644 --- a/examples/json-schema-example/tests/artifacts.test.ts +++ b/examples/json-schema-example/tests/artifacts.test.ts @@ -1,31 +1,13 @@ -import { createServer } from 'http'; -import { AddressInfo } from 'net'; import { join } from 'path'; import { DEFAULT_CLI_PARAMS, serveMesh } from '@graphql-mesh/cli'; import { fs } from '@graphql-mesh/cross-helpers'; import { Logger } from '@graphql-mesh/types'; import { fetch } from '@whatwg-node/fetch'; import { TerminateHandler } from '../../../packages/legacy/utils/dist/typings/registerTerminateHandler'; +import { getAvailablePort } from '../../../packages/testing/getAvailablePort'; const { readFile } = fs.promises; -const getFreePort = () => - new Promise((resolve, reject) => { - const server = createServer(); - server.once('error', reject); - server.listen(0, () => { - const port = (server.address() as AddressInfo)?.port; - server.closeAllConnections(); - server.close(err => { - if (err) { - reject(err); - } else { - resolve(port); - } - }); - }); - }); - describe('Artifacts', () => { it('should execute queries', async () => { const { getBuiltMesh } = await import('../.mesh/index'); @@ -70,7 +52,7 @@ describe('Artifacts', () => { log: jest.fn(), child: jest.fn(() => mockLogger), }; - const PORT = await getFreePort(); + const PORT = await getAvailablePort(); await serveMesh( { baseDir: join(__dirname, '..'), diff --git a/packages/cache/redis/package.json b/packages/cache/redis/package.json index 0529fcf563964..80361ade4e69f 100644 --- a/packages/cache/redis/package.json +++ b/packages/cache/redis/package.json @@ -40,6 +40,7 @@ }, "dependencies": { "@graphql-mesh/string-interpolation": "0.5.5", + "@whatwg-node/disposablestack": "^0.0.1", "ioredis": "^5.3.2", "ioredis-mock": "^8.8.3" }, diff --git a/packages/cache/redis/src/index.ts b/packages/cache/redis/src/index.ts index 7de812b810f77..c1d4b0aae07e9 100644 --- a/packages/cache/redis/src/index.ts +++ b/packages/cache/redis/src/index.ts @@ -9,6 +9,7 @@ import type { MeshPubSub, YamlConfig, } from '@graphql-mesh/types'; +import { DisposableSymbols } from '@whatwg-node/disposablestack'; function interpolateStrWithEnv(str: string): string { return stringInterpolator.parse(str, { env: process.env }); @@ -62,7 +63,7 @@ export default class RedisCache implements KeyValueCache, Disposa }); } - [Symbol.dispose](): void { + [DisposableSymbols.dispose](): void { this.client.disconnect(); } diff --git a/packages/fusion/composition/tests/loaders.spec.ts b/packages/fusion/composition/tests/loaders.spec.ts index 3d50ca05ae5ed..ae686dfe32037 100644 --- a/packages/fusion/composition/tests/loaders.spec.ts +++ b/packages/fusion/composition/tests/loaders.spec.ts @@ -42,6 +42,7 @@ describe('Loaders', () => { plugins() { return [useCustomFetch(mockFetch)]; }, + logging: !!process.env.DEBUG, }); const res = await runtime.fetch('/graphql', { method: 'POST', diff --git a/packages/fusion/runtime/package.json b/packages/fusion/runtime/package.json index 58674479f1be8..6b187dc42841c 100644 --- a/packages/fusion/runtime/package.json +++ b/packages/fusion/runtime/package.json @@ -63,8 +63,8 @@ "@graphql-tools/stitching-directives": "^3.0.2", "@graphql-tools/utils": "^10.2.3", "@graphql-tools/wrap": "^10.0.5", + "@whatwg-node/disposablestack": "^0.0.1", "change-case": "^4.1.2", - "disposablestack": "^1.1.6", "graphql-yoga": "^5.6.0", "tslib": "^2.4.0" }, diff --git a/packages/fusion/runtime/src/unifiedGraphManager.ts b/packages/fusion/runtime/src/unifiedGraphManager.ts index c1babd74b08c7..d5c3dce840814 100644 --- a/packages/fusion/runtime/src/unifiedGraphManager.ts +++ b/packages/fusion/runtime/src/unifiedGraphManager.ts @@ -1,4 +1,3 @@ -import AsyncDisposableStack from 'disposablestack/AsyncDisposableStack'; import type { DocumentNode, GraphQLSchema } from 'graphql'; import { buildASTSchema, buildSchema, isSchema } from 'graphql'; import { getInContextSDK } from '@graphql-mesh/runtime'; @@ -9,6 +8,7 @@ import { mapMaybePromise } from '@graphql-mesh/utils'; import type { SubschemaConfig } from '@graphql-tools/delegate'; import type { IResolvers, MaybePromise, TypeSource } from '@graphql-tools/utils'; import { isDocumentNode } from '@graphql-tools/utils'; +import { AsyncDisposableStack, DisposableSymbols } from '@whatwg-node/disposablestack'; import { compareSubgraphNames, handleFederationSupergraph } from './federation.js'; import { compareSchemas, @@ -203,7 +203,7 @@ export class UnifiedGraphManager { return this.getAndSetUnifiedGraph(); } - [Symbol.asyncDispose]() { + [DisposableSymbols.asyncDispose]() { return this.disposableStack.disposeAsync(); } } diff --git a/packages/fusion/runtime/src/utils.ts b/packages/fusion/runtime/src/utils.ts index 6719f0eff12eb..7e52c0c066f91 100644 --- a/packages/fusion/runtime/src/utils.ts +++ b/packages/fusion/runtime/src/utils.ts @@ -11,6 +11,7 @@ import type { } from '@graphql-mesh/transport-common'; import type { Logger } from '@graphql-mesh/types'; import { + isDisposable, iterateAsync, loggerForExecutionRequest, mapMaybePromise, @@ -399,7 +400,3 @@ export function compareSchemas( } return aStr === bStr; } - -export function isDisposable(obj: any): obj is Disposable | AsyncDisposable { - return obj?.[Symbol.dispose] != null || obj?.[Symbol.asyncDispose] != null; -} diff --git a/packages/fusion/runtime/tests/polling.test.ts b/packages/fusion/runtime/tests/polling.test.ts index e1de2c3c079b7..81c4af26bc853 100644 --- a/packages/fusion/runtime/tests/polling.test.ts +++ b/packages/fusion/runtime/tests/polling.test.ts @@ -4,6 +4,7 @@ import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import { createDefaultExecutor, type DisposableExecutor } from '@graphql-mesh/transport-common'; import { normalizedExecutor } from '@graphql-tools/executor'; import { isAsyncIterable } from '@graphql-tools/utils'; +import { DisposableSymbols } from '@whatwg-node/disposablestack'; import { UnifiedGraphManager } from '../src/unifiedGraphManager'; describe('Polling', () => { @@ -45,7 +46,7 @@ describe('Polling', () => { return { getSubgraphExecutor() { const executor: DisposableExecutor = createDefaultExecutor(schema); - executor[Symbol.asyncDispose] = disposeFn; + executor[DisposableSymbols.asyncDispose] = disposeFn; return executor; }, }; @@ -96,7 +97,7 @@ describe('Polling', () => { // Check if transport executor is disposed per schema change expect(disposeFn).toHaveBeenCalledTimes(2); - await manager[Symbol.asyncDispose](); + await manager[DisposableSymbols.asyncDispose](); // Check if transport executor is disposed on global shutdown expect(disposeFn).toHaveBeenCalledTimes(3); }); diff --git a/packages/legacy/apollo-link/test/apollo-link.test.ts b/packages/legacy/apollo-link/test/apollo-link.test.ts index e810ba8a6b722..2243eeefbb48b 100644 --- a/packages/legacy/apollo-link/test/apollo-link.test.ts +++ b/packages/legacy/apollo-link/test/apollo-link.test.ts @@ -2,6 +2,7 @@ import { parse } from 'graphql'; import { ApolloClient, InMemoryCache, type FetchResult } from '@apollo/client/core'; import type { MeshInstance } from '@graphql-mesh/runtime'; import { observableToAsyncIterable } from '@graphql-tools/utils'; +import { DisposableSymbols } from '@whatwg-node/disposablestack'; import { getTestMesh } from '../../testing/getTestMesh.js'; import { MeshApolloLink } from '../src/index.js'; @@ -12,7 +13,7 @@ function getApolloClientFromMesh(mesh: MeshInstance) { }); return { client, - [Symbol.dispose]: () => client.stop(), + [DisposableSymbols.dispose]: () => client.stop(), }; } diff --git a/packages/legacy/handlers/graphql/test/handler.spec.ts b/packages/legacy/handlers/graphql/test/handler.spec.ts index 73038b9c09d38..1de5b02fc0350 100644 --- a/packages/legacy/handlers/graphql/test/handler.spec.ts +++ b/packages/legacy/handlers/graphql/test/handler.spec.ts @@ -2,7 +2,6 @@ /* eslint-disable import/no-nodejs-modules */ import { promises as fsPromises } from 'fs'; -import { createServer, Server } from 'http'; import { Socket, type AddressInfo } from 'net'; import { join } from 'path'; import { buildASTSchema, buildSchema, introspectionFromSchema, parse } from 'graphql'; @@ -15,6 +14,7 @@ import { import { defaultImportFn, DefaultLogger, PubSub } from '@graphql-mesh/utils'; import { printSchemaWithDirectives } from '@graphql-tools/utils'; import { fetch as fetchFn } from '@whatwg-node/fetch'; +import { createDisposableServer } from '../../../../testing/createDisposableServer.js'; import GraphQLHandler from '../src/index.js'; const { readFile } = fsPromises; @@ -23,18 +23,12 @@ const logger = new DefaultLogger('tests'); describe('graphql', () => { let store: MeshStore; - const servers = new Set(); - const sockets = new Set(); beforeEach(() => { store = new MeshStore('.mesh', new InMemoryStoreStorageAdapter(), { readonly: false, validate: false, }); }); - afterEach(() => { - sockets.forEach(socket => socket.destroy()); - servers.forEach(server => server.close()); - }); it('handle SDL files correctly as endpoint', async () => { const sdlFilePath = './fixtures/schema.graphql'; const handler = new GraphQLHandler({ @@ -132,7 +126,7 @@ describe('graphql', () => { }); it('should handle fallback, retry and timeout options', async () => { let cnt = 0; - const server1 = createServer((req, res) => { + await using server1 = await createDisposableServer((req, res) => { if (cnt < 2) { const timeout = setTimeout(() => { res.writeHead(200); @@ -150,16 +144,7 @@ describe('graphql', () => { cnt++; } }); - servers.add(server1); - function socketListener(socket: Socket) { - sockets.add(socket); - socket.once('close', () => { - sockets.delete(socket); - }); - } - server1.on('connection', socketListener); - server1.listen(0); - const server2 = createServer((req, res) => { + await using server2 = await createDisposableServer((req, res) => { res.writeHead(200); res.end( JSON.stringify({ @@ -169,9 +154,6 @@ describe('graphql', () => { }), ); }); - servers.add(server2); - server2.on('connection', socketListener); - server2.listen(0); const introspectionSchemaProxy = store.proxy( 'introspectionSchema', @@ -189,12 +171,12 @@ describe('graphql', () => { config: { sources: [ { - endpoint: `http://localhost:${(server1.address() as AddressInfo).port}`, + endpoint: `http://localhost:${server1.address().port}`, timeout: 500, retry: 3, }, { - endpoint: `http://localhost:${(server2.address() as AddressInfo).port}`, + endpoint: `http://localhost:${server2.address().port}`, retry: 3, }, ], diff --git a/packages/legacy/handlers/mysql/src/index.ts b/packages/legacy/handlers/mysql/src/index.ts index 3477fd75d4dce..5e0217e5b9b74 100644 --- a/packages/legacy/handlers/mysql/src/index.ts +++ b/packages/legacy/handlers/mysql/src/index.ts @@ -10,7 +10,7 @@ import type { MeshSource, YamlConfig, } from '@graphql-mesh/types'; -import { loadFromModuleExportExpression } from '@graphql-mesh/utils'; +import { dispose, isDisposable, loadFromModuleExportExpression } from '@graphql-mesh/utils'; import { getMySQLExecutor, loadGraphQLSchemaFromMySQL } from '@omnigraph/mysql'; export default class MySQLHandler implements MeshHandler { @@ -85,10 +85,13 @@ export default class MySQLHandler implements MeshHandler { pool, }); - const id = this.pubsub.subscribe('destroy', () => { - executor[Symbol.asyncDispose](); - this.pubsub.unsubscribe(id); - }); + if (isDisposable(executor)) { + const id = this.pubsub.subscribe('destroy', () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + dispose(executor); + this.pubsub.unsubscribe(id); + }); + } return { schema, diff --git a/packages/legacy/handlers/neo4j/src/index.ts b/packages/legacy/handlers/neo4j/src/index.ts index f7bee894eb192..27ef5ab3a28e3 100644 --- a/packages/legacy/handlers/neo4j/src/index.ts +++ b/packages/legacy/handlers/neo4j/src/index.ts @@ -13,7 +13,7 @@ import type { MeshSource, YamlConfig, } from '@graphql-mesh/types'; -import { readFileOrUrl } from '@graphql-mesh/utils'; +import { dispose, isDisposable, readFileOrUrl } from '@graphql-mesh/utils'; import { getDriverFromOpts, getNeo4JExecutor, loadGraphQLSchemaFromNeo4J } from '@omnigraph/neo4j'; export default class Neo4JHandler implements MeshHandler { @@ -102,10 +102,13 @@ export default class Neo4JHandler implements MeshHandler { logger: this.logger, }); - const id = this.pubsub.subscribe('destroy', () => { - executor[Symbol.asyncDispose](); - this.pubsub.unsubscribe(id); - }); + if (isDisposable(executor)) { + const id = this.pubsub.subscribe('destroy', () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + dispose(executor); + this.pubsub.unsubscribe(id); + }); + } return { schema, diff --git a/packages/legacy/handlers/supergraph/tests/supergraph.spec.ts b/packages/legacy/handlers/supergraph/tests/supergraph.spec.ts index abb86379613b4..58eee27c9be4b 100644 --- a/packages/legacy/handlers/supergraph/tests/supergraph.spec.ts +++ b/packages/legacy/handlers/supergraph/tests/supergraph.spec.ts @@ -1,7 +1,4 @@ /* eslint-disable import/no-extraneous-dependencies */ -import { createServer } from 'node:http'; -import type { AddressInfo } from 'node:net'; -import AsyncDisposableStack from 'disposablestack/AsyncDisposableStack'; import type { ServerOptions } from 'graphql-ws/lib/server'; import { useServer } from 'graphql-ws/lib/use/ws'; import type { YogaServerInstance } from 'graphql-yoga'; @@ -14,6 +11,7 @@ import SupergraphHandler from '@graphql-mesh/supergraph'; import type { MeshFetch } from '@graphql-mesh/types'; import { defaultImportFn as importFn, PubSub } from '@graphql-mesh/utils'; import { fetch as defaultFetchFn } from '@whatwg-node/fetch'; +import { createDisposableServer } from '../../../../testing/createDisposableServer.js'; import { AUTH_HEADER as AUTHORS_AUTH_HEADER, server as authorsServer, @@ -286,38 +284,23 @@ describe('Supergraph', () => { getaddrinfo ENOTFOUND down-sdl-source.com`); }); it('configures WebSockets for subscriptions correctly', async () => { - await using disposableStack = new AsyncDisposableStack(); - const authorsHttpServer = createServer(authorsServer); - disposableStack.defer( - () => - new Promise((resolve, reject) => - authorsHttpServer.close(err => (err ? reject(err) : resolve())), - ), - ); + await using authorsHttpServer = await createDisposableServer(authorsServer); useServer( getGraphQLWSOptionsForYoga(authorsServer), new WebSocketServer({ - server: authorsHttpServer, + server: authorsHttpServer.server, path: authorsServer.graphqlEndpoint, }), ); - const booksHttpServer = createServer(booksServer); - disposableStack.defer( - () => - new Promise((resolve, reject) => - booksHttpServer.close(err => (err ? reject(err) : resolve())), - ), - ); + await using booksHttpServer = await createDisposableServer(booksServer); useServer( getGraphQLWSOptionsForYoga(booksServer), new WebSocketServer({ - server: booksHttpServer, + server: booksHttpServer.server, path: booksServer.graphqlEndpoint, }), ); - await new Promise(resolve => authorsHttpServer.listen(0, () => resolve())); - await new Promise(resolve => booksHttpServer.listen(0, () => resolve())); const handler = new SupergraphHandler({ ...baseHandlerConfig, config: { @@ -325,7 +308,7 @@ describe('Supergraph', () => { subgraphs: [ { name: 'authors', - endpoint: `http://localhost:${(authorsHttpServer.address() as AddressInfo).port}${authorsServer.graphqlEndpoint}`, + endpoint: `http://localhost:${authorsHttpServer.address().port}${authorsServer.graphqlEndpoint}`, operationHeaders: { Authorization: AUTHORS_AUTH_HEADER, }, @@ -333,7 +316,7 @@ describe('Supergraph', () => { }, { name: 'books', - endpoint: `http://localhost:${(booksHttpServer.address() as AddressInfo).port}${booksServer.graphqlEndpoint}`, + endpoint: `http://localhost:${booksHttpServer.address().port}${booksServer.graphqlEndpoint}`, operationHeaders: { Authorization: BOOKS_AUTH_HEADER, }, @@ -368,7 +351,6 @@ describe('Supergraph', () => { throw new Error('Subscription result is not an async iterable'); } const subscriptionAsyncIterator = subscriptionResult[Symbol.asyncIterator](); - disposableStack.defer(() => subscriptionAsyncIterator.return() as Promise); const subscriptionIterationResult$ = subscriptionAsyncIterator.next(); // Wait for the subscription to be ready await new Promise(resolve => setTimeout(resolve, 30)); @@ -389,6 +371,7 @@ describe('Supergraph', () => { expect(subscriptionIterationResult.value.data).toEqual({ bookCreated: mutationResult?.data?.createBook, }); + await subscriptionAsyncIterator.return(); }); }); diff --git a/packages/legacy/http/test/http.spec.ts b/packages/legacy/http/test/http.spec.ts index 0a79585b88a39..fdc0df03fec94 100644 --- a/packages/legacy/http/test/http.spec.ts +++ b/packages/legacy/http/test/http.spec.ts @@ -1,23 +1,14 @@ -import type { IncomingMessage, Server } from 'http'; -import { createServer, request } from 'http'; -import type { AddressInfo } from 'node:net'; -import { gunzipSync, gzip, gzipSync } from 'zlib'; +import type { IncomingMessage } from 'http'; +import { request } from 'http'; +import { gunzipSync, gzipSync } from 'zlib'; import type { ExecutionResult } from 'graphql'; import { createMeshHTTPHandler } from '@graphql-mesh/http'; import type { MeshPlugin } from '@graphql-mesh/types'; import { useContentEncoding } from '@whatwg-node/server'; +import { createDisposableServer } from '../../../testing/createDisposableServer.js'; import { getTestMesh } from '../../testing/getTestMesh.js'; describe('http', () => { - let server: Server; - afterEach(done => { - if (server) { - server.closeAllConnections(); - server.close(done); - } else { - done(); - } - }); it('should not allow upper directory access when `staticFiles` is set', async () => { await using mesh = await getTestMesh(); const httpHandler = createMeshHTTPHandler({ @@ -176,9 +167,8 @@ describe('http', () => { baseDir: __dirname, getBuiltMesh: async () => mesh, }); - server = createServer(httpHandler); - await new Promise(resolve => server.listen(0, resolve)); - const addressInfo = server.address() as AddressInfo; + await using server = await createDisposableServer(httpHandler); + const addressInfo = server.address(); const req = request({ host: 'localhost', port: addressInfo.port, diff --git a/packages/legacy/runtime/src/get-mesh.ts b/packages/legacy/runtime/src/get-mesh.ts index 25b5af4b23a3b..49579b2f7ea7a 100644 --- a/packages/legacy/runtime/src/get-mesh.ts +++ b/packages/legacy/runtime/src/get-mesh.ts @@ -22,6 +22,7 @@ import { DefaultLogger, getHeadersObj, groupTransforms, + makeDisposable, mapMaybePromise, parseWithCache, PubSub, @@ -388,29 +389,31 @@ export async function getMesh(options: GetMeshOptions): Promise { return pubsub.publish('destroy', undefined); } - return { - get schema() { - return subschema ? subschema.transformedSchema : unifiedSubschema.schema; - }, - rawSources, - cache, - pubsub, - destroy: meshDestroy, - logger, - plugins, - get getEnveloped() { - return memoizedGetEnvelopedFactory(plugins); - }, - createExecutor, - get execute() { - return createExecutor(); - }, - get subscribe() { - return createExecutor(); + return makeDisposable( + { + get schema() { + return subschema ? subschema.transformedSchema : unifiedSubschema.schema; + }, + rawSources, + cache, + pubsub, + destroy: meshDestroy, + logger, + plugins, + get getEnveloped() { + return memoizedGetEnvelopedFactory(plugins); + }, + createExecutor, + get execute() { + return createExecutor(); + }, + get subscribe() { + return createExecutor(); + }, + sdkRequesterFactory, }, - sdkRequesterFactory, - [Symbol.dispose]: meshDestroy, - }; + meshDestroy, + ); } function extractDataOrThrowErrors(result: ExecutionResult): T { diff --git a/packages/legacy/utils/package.json b/packages/legacy/utils/package.json index d1f862a8620aa..f83e2197b813a 100644 --- a/packages/legacy/utils/package.json +++ b/packages/legacy/utils/package.json @@ -41,8 +41,8 @@ "dependencies": { "@graphql-mesh/string-interpolation": "^0.5.5", "@graphql-tools/delegate": "^10.0.16", + "@whatwg-node/disposablestack": "^0.0.1", "@whatwg-node/fetch": "^0.9.13", - "disposablestack": "^1.1.6", "dset": "^3.1.2", "js-yaml": "^4.1.0", "lodash.get": "^4.4.2", diff --git a/packages/legacy/utils/src/disposable.ts b/packages/legacy/utils/src/disposable.ts new file mode 100644 index 0000000000000..e1ebc07766468 --- /dev/null +++ b/packages/legacy/utils/src/disposable.ts @@ -0,0 +1,49 @@ +import type { MaybePromise } from '@graphql-tools/utils'; +import { DisposableSymbols } from '@whatwg-node/disposablestack'; + +export function isDisposable(obj: any): obj is Disposable | AsyncDisposable { + return obj?.[DisposableSymbols.dispose] || obj?.[DisposableSymbols.asyncDispose]; +} + +export function dispose(disposable: T): MaybePromise; +export function dispose(disposable: T): void; +export function dispose(disposable: T): MaybePromise; +export function dispose(disposable: T): MaybePromise { + if (DisposableSymbols.dispose in disposable) { + return disposable[DisposableSymbols.dispose](); + } + if (DisposableSymbols.asyncDispose in disposable) { + return disposable[DisposableSymbols.asyncDispose](); + } +} + +export function createDisposable(disposeFn: () => void): Disposable { + return { + [DisposableSymbols.dispose]: disposeFn, + }; +} + +export function createAsyncDisposable(disposeFn: () => Promise): AsyncDisposable { + return { + [DisposableSymbols.asyncDispose]: disposeFn, + }; +} + +export function makeDisposable(obj: T, disposeFn: () => void): T & Disposable { + Object.defineProperty(obj, DisposableSymbols.dispose, { + value: disposeFn, + configurable: true, + }); + return obj as T & Disposable; +} + +export function makeAsyncDisposable( + obj: T, + disposeFn: () => Promise, +): T & AsyncDisposable { + Object.defineProperty(obj, DisposableSymbols.asyncDispose, { + value: disposeFn, + configurable: true, + }); + return obj as T & AsyncDisposable; +} diff --git a/packages/legacy/utils/src/index.ts b/packages/legacy/utils/src/index.ts index 843dce9b62330..370935a241c8e 100644 --- a/packages/legacy/utils/src/index.ts +++ b/packages/legacy/utils/src/index.ts @@ -25,3 +25,4 @@ export * from './registerTerminateHandler.js'; export * from './getAdditionalResolversFromTypeDefs.js'; export * from './get-def-directives.js'; export * from './getDirectiveExtensions.js'; +export * from './disposable.js'; diff --git a/packages/legacy/utils/src/registerTerminateHandler.ts b/packages/legacy/utils/src/registerTerminateHandler.ts index 3115d09ad15c6..48fe74cc6461a 100644 --- a/packages/legacy/utils/src/registerTerminateHandler.ts +++ b/packages/legacy/utils/src/registerTerminateHandler.ts @@ -1,4 +1,4 @@ -import AsyncDisposableStack from 'disposablestack/AsyncDisposableStack'; +import { AsyncDisposableStack } from '@whatwg-node/disposablestack'; const terminateEvents = ['SIGINT', 'SIGTERM'] as const; diff --git a/packages/loaders/json-schema/test/timeout.test.ts b/packages/loaders/json-schema/test/timeout.test.ts index ad9d939137c0b..23f102a6927af 100644 --- a/packages/loaders/json-schema/test/timeout.test.ts +++ b/packages/loaders/json-schema/test/timeout.test.ts @@ -1,29 +1,25 @@ /* eslint-disable import/no-nodejs-modules */ -import { createServer, Server } from 'http'; -import type { AddressInfo } from 'net'; import { execute, OperationTypeNode, parse } from 'graphql'; +import { createDisposableServer } from '../../../testing/createDisposableServer.js'; import { loadGraphQLSchemaFromJSONSchemas } from '../src/loadGraphQLSchemaFromJSONSchemas.js'; describe('Timeout', () => { - let server: Server; let timeout: NodeJS.Timeout; - beforeAll(async () => { - server = createServer((req, res) => { + afterEach(() => { + if (timeout) { + clearTimeout(timeout); + } + }); + it('should timeout correctly', async () => { + await using server = await createDisposableServer((req, res) => { timeout = setTimeout(() => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('test'); }, 500); }); - await new Promise(resolve => server.listen(0, resolve)); - }); - afterAll(async () => { - clearTimeout(timeout); - await new Promise(resolve => server.close(resolve)); - }); - it('should timeout correctly', async () => { const schema = await loadGraphQLSchemaFromJSONSchemas('test', { timeout: 300, - endpoint: `http://localhost:${(server.address() as AddressInfo).port}`, + endpoint: `http://localhost:${server.address().port}`, operations: [ { type: OperationTypeNode.QUERY, diff --git a/packages/loaders/openapi/tests/spotify.test.ts b/packages/loaders/openapi/tests/spotify.test.ts index 10842dfea3956..49ca12aef634e 100644 --- a/packages/loaders/openapi/tests/spotify.test.ts +++ b/packages/loaders/openapi/tests/spotify.test.ts @@ -11,27 +11,16 @@ describe('Spotify', () => { source: './fixtures/spotify.yml', cwd: __dirname, ignoreErrorResponses: true, - fetch: (url): Promise => { - return Promise.resolve( - new Response( - JSON.stringify({ - albums: { - items: [ - { - name: url, - }, - ], - }, - }), - { - status: 200, - headers: { - 'Content-Type': 'application/json', + fetch: async url => + Response.json({ + albums: { + items: [ + { + name: url, }, - }, - ), - ); - }, + ], + }, + }), }); }); diff --git a/packages/plugins/hive/package.json b/packages/plugins/hive/package.json index f4f8aecf50706..974e17fd888d0 100644 --- a/packages/plugins/hive/package.json +++ b/packages/plugins/hive/package.json @@ -34,6 +34,7 @@ "peerDependencies": { "@graphql-mesh/cross-helpers": "^0.4.4", "@graphql-mesh/types": "^0.99.4", + "@graphql-mesh/utils": "^0.99.4", "graphql": "*", "tslib": "^2.4.0" }, diff --git a/packages/plugins/hive/src/index.ts b/packages/plugins/hive/src/index.ts index dc44867ca81eb..d63663c45600d 100644 --- a/packages/plugins/hive/src/index.ts +++ b/packages/plugins/hive/src/index.ts @@ -3,6 +3,7 @@ import { createHive, useHive } from '@graphql-hive/yoga'; import { process } from '@graphql-mesh/cross-helpers'; import { stringInterpolator } from '@graphql-mesh/string-interpolation'; import type { Logger, MeshPlugin, MeshPubSub, YamlConfig } from '@graphql-mesh/types'; +import { makeAsyncDisposable } from '@graphql-mesh/utils'; export default function useMeshHive( pluginOptions: YamlConfig.HivePlugin & { @@ -122,15 +123,15 @@ export default function useMeshHive( onTerminate().finally(() => pluginOptions.pubsub.unsubscribe(id)), ); - return { - onPluginInit({ addPlugin }) { - addPlugin( - // TODO: fix useYogaHive typings to inherit the context - useHive(hiveClient) as any, - ); + return makeAsyncDisposable( + { + onPluginInit({ addPlugin }) { + addPlugin( + // TODO: fix useYogaHive typings to inherit the context + useHive(hiveClient) as any, + ); + }, }, - [Symbol.asyncDispose]() { - return onTerminate(); - }, - }; + onTerminate, + ); } diff --git a/packages/serve-cli/src/nodeHttp.ts b/packages/serve-cli/src/nodeHttp.ts index 7c425a16f5a7a..b2a63be4f8bf4 100644 --- a/packages/serve-cli/src/nodeHttp.ts +++ b/packages/serve-cli/src/nodeHttp.ts @@ -3,6 +3,7 @@ import { createServer as createHTTPServer } from 'node:http'; import { createServer as createHTTPSServer } from 'node:https'; import type { SecureContextOptions } from 'node:tls'; import type { RecognizedString } from 'uWebSockets.js'; +import { createAsyncDisposable } from '@graphql-mesh/utils'; import type { ServerOptions } from './types.js'; export function readRecognizedString(recognizedString: RecognizedString) { @@ -59,18 +60,19 @@ export async function startNodeHttpServer({ server.once('error', reject); server.listen(port, host, () => { log.info(`Server started on ${protocol}://${host}:${port}`); - resolve({ - [Symbol.asyncDispose]() { - return new Promise(resolve => { - log.info(`Closing server`); - server.closeAllConnections(); - server.close(() => { - log.info(`Server closed`); - resolve(); - }); - }); - }, - }); + resolve( + createAsyncDisposable( + () => + new Promise(resolve => { + log.info(`Closing server`); + server.closeAllConnections(); + server.close(() => { + log.info(`Server closed`); + resolve(); + }); + }), + ), + ); }); }); } @@ -85,18 +87,19 @@ export async function startNodeHttpServer({ server.once('error', reject); server.listen(port, host, () => { log.info(`Server started on ${protocol}://${host}:${port}`); - resolve({ - [Symbol.asyncDispose]() { - return new Promise(resolve => { - log.info(`Closing server`); - server.closeAllConnections(); - server.close(() => { - log.info(`Server closed`); - resolve(); - }); - }); - }, - }); + resolve( + createAsyncDisposable( + () => + new Promise(resolve => { + log.info(`Closing server`); + server.closeAllConnections(); + server.close(() => { + log.info(`Server closed`); + resolve(); + }); + }), + ), + ); }); }); } diff --git a/packages/serve-cli/src/run.ts b/packages/serve-cli/src/run.ts index 1dd8e97809525..ebe426760d407 100644 --- a/packages/serve-cli/src/run.ts +++ b/packages/serve-cli/src/run.ts @@ -178,8 +178,14 @@ export async function run({ log.error(err); return; } - if (events.some(event => event.path === absoluteUnifiedGraphPath)) { - log.info(`Supergraph changed`); + if ( + events.some( + event => event.path === absoluteUnifiedGraphPath && event.type === 'update', + ) + ) { + log.info( + `Supergraph: ${unifiedGraphPath} updated on the filesystem. Invalidating...`, + ); if (fork > 1) { for (const workerId in cluster.workers) { cluster.workers[workerId].send('invalidateUnifiedGraph'); diff --git a/packages/serve-cli/src/uWebSockets.ts b/packages/serve-cli/src/uWebSockets.ts index 76be0c78b89b2..f0fc69b17be1c 100644 --- a/packages/serve-cli/src/uWebSockets.ts +++ b/packages/serve-cli/src/uWebSockets.ts @@ -1,3 +1,4 @@ +import { createDisposable } from '@graphql-mesh/utils'; import type { ServerOptions } from './types.js'; export async function startuWebSocketsServer({ @@ -17,12 +18,12 @@ export async function startuWebSocketsServer({ return new Promise((resolve, reject) => { app.listen(host, port, function listenCallback(listenSocket) { if (listenSocket) { - resolve({ - [Symbol.dispose]() { + resolve( + createDisposable(() => { log.info(`Closing ${protocol}://${host}:${port}`); app.close(); - }, - }); + }), + ); } else { reject(new Error(`Failed to start server on ${protocol}://${host}:${port}!`)); } diff --git a/packages/serve-runtime/package.json b/packages/serve-runtime/package.json index 3b8856d0be970..0dc0ae217bbf3 100644 --- a/packages/serve-runtime/package.json +++ b/packages/serve-runtime/package.json @@ -52,7 +52,7 @@ "@graphql-tools/federation": "^2.2.1", "@graphql-tools/stitch": "^9.2.10", "@graphql-tools/utils": "^10.2.3", - "disposablestack": "^1.1.6", + "@whatwg-node/disposablestack": "^0.0.1", "graphql-yoga": "^5.6.0" }, "devDependencies": { diff --git a/packages/serve-runtime/src/createServeRuntime.ts b/packages/serve-runtime/src/createServeRuntime.ts index 414196b1de0dc..e2059a890ea30 100644 --- a/packages/serve-runtime/src/createServeRuntime.ts +++ b/packages/serve-runtime/src/createServeRuntime.ts @@ -1,13 +1,10 @@ // eslint-disable-next-line import/no-nodejs-modules import type { IncomingMessage } from 'node:http'; -import AsyncDisposableStack from 'disposablestack/AsyncDisposableStack'; import type { GraphQLSchema } from 'graphql'; import { parse } from 'graphql'; import { - createGraphQLError, createYoga, isAsyncIterable, - Repeater, useReadinessCheck, type FetchAPI, type LandingPageRenderer, @@ -15,31 +12,28 @@ import { type YogaServerInstance, } from 'graphql-yoga'; import type { GraphiQLOptionsOrFactory } from 'graphql-yoga/typings/plugins/use-graphiql.js'; -import type { AsyncIterableIteratorOrValue } from '@envelop/core'; import { createSupergraphSDLFetcher } from '@graphql-hive/apollo'; import { process } from '@graphql-mesh/cross-helpers'; import type { OnSubgraphExecuteHook, - TransportEntry, UnifiedGraphManagerOptions, } from '@graphql-mesh/fusion-runtime'; -import { - handleFederationSupergraph, - isDisposable, - UnifiedGraphManager, -} from '@graphql-mesh/fusion-runtime'; +import { handleFederationSupergraph, UnifiedGraphManager } from '@graphql-mesh/fusion-runtime'; import useMeshHive from '@graphql-mesh/plugin-hive'; // eslint-disable-next-line import/no-extraneous-dependencies import type { Logger, MeshPlugin, OnDelegateHook, OnFetchHook } from '@graphql-mesh/types'; import { DefaultLogger, getHeadersObj, + isDisposable, LogLevel, + makeAsyncDisposable, mapMaybePromise, wrapFetchWithHooks, } from '@graphql-mesh/utils'; import { useExecutor } from '@graphql-tools/executor-yoga'; import type { MaybePromise } from '@graphql-tools/utils'; +import { AsyncDisposableStack } from '@whatwg-node/disposablestack'; import { getProxyExecutor } from './getProxyExecutor.js'; import { handleUnifiedGraphConfig } from './handleUnifiedGraphConfig.js'; import landingPageHtml from './landing-page-html.js'; @@ -409,13 +403,12 @@ export function createServeRuntime = Record value: schemaInvalidator, configurable: true, }, - [Symbol.asyncDispose]: { - value: () => disposableStack.disposeAsync(), - configurable: true, - }, }); - return yoga as YogaServerInstance & { - invalidateUnifiedGraph(): void; - } & AsyncDisposable; + return makeAsyncDisposable( + yoga as YogaServerInstance & { + invalidateUnifiedGraph(): void; + }, + () => disposableStack.disposeAsync(), + ); } diff --git a/packages/serve-runtime/tests/hive.spec.ts b/packages/serve-runtime/tests/hive.spec.ts index 2b88d76a588d6..4da390725258e 100644 --- a/packages/serve-runtime/tests/hive.spec.ts +++ b/packages/serve-runtime/tests/hive.spec.ts @@ -1,7 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies */ import { createServer } from 'http'; import type { AddressInfo } from 'net'; -import AsyncDisposableStack from 'disposablestack/AsyncDisposableStack'; import { buildClientSchema, getIntrospectionQuery, @@ -11,6 +10,7 @@ import { } from 'graphql'; import { createSchema } from 'graphql-yoga'; import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; +import { createDisposableServer } from '../../testing/createDisposableServer.js'; import { createServeRuntime } from '../src/createServeRuntime.js'; function createUpstreamSchema() { @@ -31,16 +31,13 @@ function createUpstreamSchema() { }); } -const leftovers = new Set<() => PromiseLike>(); -afterAll(() => Promise.all(Array.from(leftovers).map(l => l())).finally(() => leftovers.clear())); - describe('Hive CDN', () => { afterEach(() => { delete process.env.HIVE_CDN_ENDPOINT; delete process.env.HIVE_CDN_KEY; }); it('respects env vars', async () => { - const cdnServer = createServer((_req, res) => { + await using cdnServer = await createDisposableServer((_req, res) => { const supergraph = getUnifiedGraphGracefully([ { name: 'upstream', @@ -49,17 +46,9 @@ describe('Hive CDN', () => { ]); res.end(supergraph); }); - cdnServer.listen(); - leftovers.add( - () => - new Promise((resolve, reject) => { - cdnServer.close(err => (err ? reject(err) : resolve())); - }), - ); - process.env.HIVE_CDN_ENDPOINT = `http://localhost:${(cdnServer.address() as AddressInfo).port}`; + process.env.HIVE_CDN_ENDPOINT = `http://localhost:${cdnServer.address().port}`; process.env.HIVE_CDN_KEY = 'key'; await using serveRuntime = createServeRuntime(); - leftovers.add(() => serveRuntime[Symbol.asyncDispose]()); const res = await serveRuntime.fetch('http://localhost:4000/graphql', { method: 'POST', headers: { diff --git a/packages/serve-runtime/tests/serve-runtime.spec.ts b/packages/serve-runtime/tests/serve-runtime.spec.ts index 2d295eb24dc76..58009a0b4f2a7 100644 --- a/packages/serve-runtime/tests/serve-runtime.spec.ts +++ b/packages/serve-runtime/tests/serve-runtime.spec.ts @@ -11,6 +11,7 @@ import { import { createSchema, createYoga } from 'graphql-yoga'; import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import { buildHTTPExecutor } from '@graphql-tools/executor-http'; +import { DisposableSymbols } from '@whatwg-node/disposablestack'; import { Response } from '@whatwg-node/server'; import { createServeRuntime } from '../src/createServeRuntime.js'; import type { MeshServePlugin } from '../src/types.js'; @@ -242,12 +243,12 @@ describe('Serve Runtime', () => { plugins: () => [ { onSchemaChange() { - if (onSchemaChangeCalls > 0) { + if (onSchemaChangeCalls === 1) { // schema changed for the second time done(); - serve[Symbol.asyncDispose](); } onSchemaChangeCalls++; + serve[DisposableSymbols.asyncDispose](); }, }, ], diff --git a/packages/serve-runtime/tests/subscriptions.test.ts b/packages/serve-runtime/tests/subscriptions.test.ts index 118c8fae3c230..14bf75057dfae 100644 --- a/packages/serve-runtime/tests/subscriptions.test.ts +++ b/packages/serve-runtime/tests/subscriptions.test.ts @@ -3,12 +3,12 @@ import { createSchema, createYoga, Repeater } from 'graphql-yoga'; import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import { buildHTTPExecutor } from '@graphql-tools/executor-http'; import { type MaybePromise } from '@graphql-tools/utils'; +import { DisposableSymbols } from '@whatwg-node/disposablestack'; import { createServeRuntime } from '../src/createServeRuntime'; -const leftovers: (() => MaybePromise)[] = []; -afterAll(() => Promise.all(leftovers.map(l => l()))); - describe('Subscriptions', () => { + const leftovers: (() => MaybePromise)[] = []; + afterAll(() => Promise.all(leftovers.map(l => l()))); const upstreamSchema = createSchema({ typeDefs: /* GraphQL */ ` """ @@ -67,7 +67,7 @@ describe('Subscriptions', () => { on: { connected() { setImmediate(() => { - serve[Symbol.asyncDispose](); + serve[DisposableSymbols.asyncDispose](); }); }, }, diff --git a/packages/serve-runtime/tests/useUpstreamCancel.spec.ts b/packages/serve-runtime/tests/useUpstreamCancel.spec.ts index 3087cbc6d5fdd..73cff575a034d 100644 --- a/packages/serve-runtime/tests/useUpstreamCancel.spec.ts +++ b/packages/serve-runtime/tests/useUpstreamCancel.spec.ts @@ -1,26 +1,14 @@ /* eslint-disable import/no-nodejs-modules */ /* eslint-disable import/no-extraneous-dependencies */ -import { createServer, type Server } from 'http'; import type { AddressInfo } from 'net'; import { createSchema, createYoga } from 'graphql-yoga'; import { fetch } from '@whatwg-node/fetch'; import { createServerAdapter, Response } from '@whatwg-node/server'; +import { createDisposableServer } from '../../testing/createDisposableServer'; import { createServeRuntime } from '../src/createServeRuntime'; import { useUpstreamCancel } from '../src/useUpstreamCancel'; describe('useUpstreamCancel', () => { - const serversToClose = new Set(); - afterAll(() => - Promise.all( - [...serversToClose].map( - server => - new Promise((resolve, reject) => { - server.closeAllConnections(); - server.close(err => (err ? reject(err) : resolve())); - }), - ), - ), - ); it('cancels upstream requests when the client cancels', async () => { const serveRuntimeFetchCallAbortCtrl = new AbortController(); let resolveDataSource: (response: Response) => void; @@ -35,13 +23,7 @@ describe('useUpstreamCancel', () => { resolveDataSource = resolve; }); }); - const dataSourceServer = createServer(dataSourceAdapter); - await new Promise(resolve => - dataSourceServer.listen(0, () => { - serversToClose.add(dataSourceServer); - resolve(); - }), - ); + await using dataSourceServer = await createDisposableServer(dataSourceAdapter); const upstreamGraphQL = createYoga({ logging: false, schema: createSchema({ @@ -53,51 +35,36 @@ describe('useUpstreamCancel', () => { resolvers: { Query: { hello: (_root, _args, context) => - fetch(`http://localhost:${(dataSourceServer.address() as AddressInfo).port}`, { + fetch(`http://localhost:${dataSourceServer.address().port}`, { signal: context.request.signal, }).then(dataSourceFetchSpy), }, }, }), }); - const upstreamGraphQLServer = createServer(upstreamGraphQL); - await new Promise(resolve => - upstreamGraphQLServer.listen(0, () => { - serversToClose.add(upstreamGraphQLServer); - resolve(); - }), - ); + await using upstreamGraphQLServer = await createDisposableServer(upstreamGraphQL); await using serveRuntime = createServeRuntime({ proxy: { - endpoint: `http://localhost:${(upstreamGraphQLServer.address() as AddressInfo).port}/graphql`, + endpoint: `http://localhost:${upstreamGraphQLServer.address().port}/graphql`, }, plugins: () => [useUpstreamCancel()], logging: false, }); - const serveRuntimeServer = createServer(serveRuntime); - await new Promise(resolve => - serveRuntimeServer.listen(0, () => { - serversToClose.add(serveRuntimeServer); - resolve(); - }), - ); - const res$ = fetch( - `http://localhost:${(serveRuntimeServer.address() as AddressInfo).port}/graphql`, - { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ - query: /* GraphQL */ ` - query { - hello - } - `, - }), - signal: serveRuntimeFetchCallAbortCtrl.signal, + await using serveRuntimeServer = await createDisposableServer(serveRuntime); + const res$ = fetch(`http://localhost:${serveRuntimeServer.address().port}/graphql`, { + method: 'POST', + headers: { + 'content-type': 'application/json', }, - ); + body: JSON.stringify({ + query: /* GraphQL */ ` + query { + hello + } + `, + }), + signal: serveRuntimeFetchCallAbortCtrl.signal, + }); await expect(res$).rejects.toThrow(); expect(dataSourceFetchSpy).not.toHaveBeenCalled(); await new Promise(resolve => setTimeout(resolve, 300)); diff --git a/packages/testing/createDisposableServer.ts b/packages/testing/createDisposableServer.ts new file mode 100644 index 0000000000000..31588527fc60a --- /dev/null +++ b/packages/testing/createDisposableServer.ts @@ -0,0 +1,41 @@ +import { createServer } from 'node:http'; +import type { RequestListener } from 'node:http'; +import type { AddressInfo, Socket } from 'node:net'; +import { DisposableSymbols } from '@whatwg-node/disposablestack'; + +export interface DisposableServerOpts { + port?: number; +} + +export async function createDisposableServer( + requestListener?: RequestListener, + opts?: DisposableServerOpts, +) { + const server = createServer(requestListener); + const port = opts?.port || 0; + await new Promise(resolve => server.listen(port, () => resolve())); + const sockets = new Set(); + server.on('connection', socket => { + sockets.add(socket); + socket.once('close', () => { + sockets.delete(socket); + }); + }); + return { + address() { + return server.address() as AddressInfo; + }, + [DisposableSymbols.asyncDispose]() { + for (const socket of sockets) { + socket.destroy(); + } + server.closeAllConnections(); + return new Promise((resolve, reject) => { + server.close(err => (err ? reject(err) : resolve())); + }); + }, + get server() { + return server; + }, + }; +} diff --git a/packages/testing/disposableExec.ts b/packages/testing/disposableExec.ts new file mode 100644 index 0000000000000..cf76728ee7a0d --- /dev/null +++ b/packages/testing/disposableExec.ts @@ -0,0 +1,17 @@ +import { exec } from 'child_process'; +import { DisposableSymbols } from '@whatwg-node/disposablestack'; + +export function disposableExec(...params: Parameters) { + const cmd = exec(...params); + return { + get stderr() { + return cmd.stderr; + }, + get stdout() { + return cmd.stdout; + }, + [DisposableSymbols.dispose]() { + cmd.kill(); + }, + }; +} diff --git a/packages/testing/getAvailablePort.ts b/packages/testing/getAvailablePort.ts new file mode 100644 index 0000000000000..f41582ed866bc --- /dev/null +++ b/packages/testing/getAvailablePort.ts @@ -0,0 +1,10 @@ +import { createServer } from 'http'; +import type { AddressInfo } from 'net'; + +export async function getAvailablePort() { + const server = createServer(); + await new Promise(resolve => server.listen(0, resolve)); + const port = (server.address() as AddressInfo).port; + await new Promise(resolve => server.close(resolve)); + return port; +} diff --git a/packages/transports/http-callback/package.json b/packages/transports/http-callback/package.json index c77fc17ebad1e..b7359c7f413da 100644 --- a/packages/transports/http-callback/package.json +++ b/packages/transports/http-callback/package.json @@ -39,6 +39,7 @@ "@graphql-mesh/cross-helpers": "^0.4.4", "@graphql-mesh/string-interpolation": "^0.5.5", "@graphql-mesh/transport-common": "^0.4.4", + "@graphql-mesh/utils": "^0.99.4", "@graphql-tools/utils": "^10.2.3", "@repeaterjs/repeater": "^3.0.6", "@whatwg-node/fetch": "^0.9.18" diff --git a/packages/transports/http-callback/src/index.ts b/packages/transports/http-callback/src/index.ts index daad3ddc1a240..6ea1f5a173ef8 100644 --- a/packages/transports/http-callback/src/index.ts +++ b/packages/transports/http-callback/src/index.ts @@ -1,16 +1,14 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ import type { ExecutionResult, GraphQLError } from 'graphql'; import { process } from '@graphql-mesh/cross-helpers'; -import { - getInterpolatedHeadersFactory, - getInterpolatedStringFactory, -} from '@graphql-mesh/string-interpolation'; +import { getInterpolatedHeadersFactory } from '@graphql-mesh/string-interpolation'; import { defaultPrintFn, type DisposableExecutor, type Transport, } from '@graphql-mesh/transport-common'; -import { createGraphQLError } from '@graphql-tools/utils'; +import { makeDisposable } from '@graphql-mesh/utils'; +import { createGraphQLError, type ExecutionRequest } from '@graphql-tools/utils'; import { Repeater, type Push } from '@repeaterjs/repeater'; import { crypto } from '@whatwg-node/fetch'; @@ -64,7 +62,7 @@ function createTimeoutError() { } export default { - getSubgraphExecutor({ transportEntry, fetch, pubsub, logger }) { + getSubgraphExecutor({ transportEntry, fetch, pubsub, logger }): DisposableExecutor { let headersInConfig: Record | undefined; if (typeof transportEntry.headers === 'string') { headersInConfig = JSON.parse(transportEntry.headers); @@ -90,7 +88,7 @@ export default { const publicUrl = transportEntry.options?.public_url || 'http://localhost:4000'; const callbackPath = transportEntry.options?.path || '/callback'; const heartbeatIntervalMs = transportEntry.options.heartbeat_interval || 50000; - const httpCallbackExecutor: DisposableExecutor = function httpCallbackExecutor(execReq) { + const httpCallbackExecutor = function httpCallbackExecutor(execReq: ExecutionRequest) { const query = defaultPrintFn(execReq.document); const subscriptionId = crypto.randomUUID(); const subscriptionLogger = logger.child(subscriptionId); @@ -268,8 +266,6 @@ export default { } } } - httpCallbackExecutor[Symbol.asyncDispose] = disposeFn; - httpCallbackExecutor[Symbol.dispose] = disposeFn; - return httpCallbackExecutor; + return makeDisposable(httpCallbackExecutor, disposeFn); }, } satisfies Transport<'http-callback', HTTPCallbackTransportOptions>; diff --git a/packages/transports/http/src/index.ts b/packages/transports/http/src/index.ts index ea71243c44c73..cccdaedbca595 100644 --- a/packages/transports/http/src/index.ts +++ b/packages/transports/http/src/index.ts @@ -1,13 +1,12 @@ import { getInterpolatedHeadersFactory } from '@graphql-mesh/string-interpolation'; import { defaultPrintFn, - type DisposableExecutor, type Transport, type TransportEntry, } from '@graphql-mesh/transport-common'; -import { mapMaybePromise } from '@graphql-mesh/utils'; +import { dispose, isDisposable, makeAsyncDisposable, mapMaybePromise } from '@graphql-mesh/utils'; import { buildHTTPExecutor, type HTTPExecutorOptions } from '@graphql-tools/executor-http'; -import type { DisposableAsyncExecutor } from '@graphql-tools/utils'; +import { type AsyncExecutor, type ExecutionRequest } from '@graphql-tools/utils'; export type HTTPTransportOptions< TSubscriptionTransportKind = string, @@ -78,19 +77,19 @@ export default { ), ); }; - const hybridExecutor: DisposableAsyncExecutor = function hybridExecutor(executionRequest) { - if (subscriptionsExecutor && executionRequest.operationType === 'subscription') { - return subscriptionsExecutor(executionRequest); - } - return httpExecutor(executionRequest); - }; - hybridExecutor[Symbol.asyncDispose] = function executorDisposeFn() { - return Promise.all([ - httpExecutor[Symbol.asyncDispose]?.(), - subscriptionsExecutor?.[Symbol.asyncDispose]?.(), - ]); - }; - return hybridExecutor; + return makeAsyncDisposable( + function hybridExecutor(executionRequest: ExecutionRequest) { + if (subscriptionsExecutor && executionRequest.operationType === 'subscription') { + return subscriptionsExecutor(executionRequest); + } + return httpExecutor(executionRequest); + }, + () => + Promise.all([ + isDisposable(httpExecutor) && dispose(httpExecutor), + isDisposable(subscriptionsExecutor) && dispose(subscriptionsExecutor), + ]).then(() => {}), + ); } return httpExecutor; diff --git a/packages/transports/mysql/src/execution.ts b/packages/transports/mysql/src/execution.ts index c38ee8a507a7d..a7c08b7232314 100644 --- a/packages/transports/mysql/src/execution.ts +++ b/packages/transports/mysql/src/execution.ts @@ -5,9 +5,9 @@ import { createPool, type Pool, type PoolConnection } from 'mysql'; import { introspection, upgrade } from 'mysql-utilities'; import { util } from '@graphql-mesh/cross-helpers'; import type { DisposableExecutor } from '@graphql-mesh/transport-common'; -import { getDefDirectives } from '@graphql-mesh/utils'; +import { getDefDirectives, makeAsyncDisposable } from '@graphql-mesh/utils'; import { createDefaultExecutor } from '@graphql-tools/delegate'; -import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'; +import { getDirective, MapperKind, mapSchema, type ExecutionRequest } from '@graphql-tools/utils'; import { getConnectionOptsFromEndpointUri } from './parseEndpointUri.js'; import type { MySQLContext } from './types.js'; @@ -189,26 +189,26 @@ export function getMySQLExecutor({ subgraph, pool }: GetMySQLExecutorOpts): Disp const defaultExecutor = createDefaultExecutor(subgraph); const getConnection$ = util.promisify(pool.getConnection.bind(pool)); - const executor: DisposableExecutor = async function mysqlExecutor(executionRequest) { - const mysqlConnection = await getConnection$(); - mysqlConnectionByContext.set(executionRequest.context, mysqlConnection); - try { - return await defaultExecutor(executionRequest); - } finally { - mysqlConnectionByContext.delete(executionRequest.context); - mysqlConnection.release(); - } - }; - executor[Symbol.asyncDispose] = function dispose() { - return new Promise((resolve, reject) => { - pool.end(err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - }; - return executor; + return makeAsyncDisposable( + async function mysqlExecutor(executionRequest: ExecutionRequest) { + const mysqlConnection = await getConnection$(); + mysqlConnectionByContext.set(executionRequest.context, mysqlConnection); + try { + return await defaultExecutor(executionRequest); + } finally { + mysqlConnectionByContext.delete(executionRequest.context); + mysqlConnection.release(); + } + }, + () => + new Promise((resolve, reject) => { + pool.end(err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }), + ); } diff --git a/packages/transports/neo4j/src/executor.ts b/packages/transports/neo4j/src/executor.ts index 5000ae9b907d1..1e55c06f99daf 100644 --- a/packages/transports/neo4j/src/executor.ts +++ b/packages/transports/neo4j/src/executor.ts @@ -4,9 +4,9 @@ import { GraphQLBigInt } from 'graphql-scalars'; import type { Driver } from 'neo4j-driver'; import type { DisposableExecutor } from '@graphql-mesh/transport-common'; import type { Logger, MeshPubSub } from '@graphql-mesh/types'; -import { getDirectiveExtensions } from '@graphql-mesh/utils'; +import { getDirectiveExtensions, makeAsyncDisposable } from '@graphql-mesh/utils'; import { createDefaultExecutor } from '@graphql-tools/delegate'; -import { asArray, getDocumentNodeFromSchema } from '@graphql-tools/utils'; +import { asArray, getDocumentNodeFromSchema, type ExecutionRequest } from '@graphql-tools/utils'; import { Neo4jGraphQL } from '@neo4j/graphql'; import { getDriverFromOpts } from './driver.js'; import { getEventEmitterFromPubSub } from './eventEmitterForPubSub.js'; @@ -84,21 +84,18 @@ export async function getNeo4JExecutor(opts: Neo4JExecutorOpts): Promise driver.close(), + ); } interface GetExecutableSchemaFromTypeDefs { diff --git a/packages/transports/ws/package.json b/packages/transports/ws/package.json index 88c8f86b97dd3..003c3a0ccea9c 100644 --- a/packages/transports/ws/package.json +++ b/packages/transports/ws/package.json @@ -39,6 +39,7 @@ "@graphql-mesh/cross-helpers": "0.4.4", "@graphql-mesh/string-interpolation": "^0.5.5", "@graphql-mesh/transport-common": "^0.4.4", + "@graphql-mesh/utils": "^0.99.4", "@graphql-tools/executor-graphql-ws": "^1.2.0", "@graphql-tools/utils": "^10.2.3", "graphql-ws": "^5.16.0", diff --git a/packages/transports/ws/src/index.ts b/packages/transports/ws/src/index.ts index a5864d422558f..7ffc3e2ad5ecc 100644 --- a/packages/transports/ws/src/index.ts +++ b/packages/transports/ws/src/index.ts @@ -6,6 +6,7 @@ import { type DisposableExecutor, type Transport, } from '@graphql-mesh/transport-common'; +import { dispose, isDisposable, makeAsyncDisposable } from '@graphql-mesh/utils'; import { buildGraphQLWSExecutor } from '@graphql-tools/executor-graphql-ws'; function switchProtocols(url: string) { @@ -81,10 +82,12 @@ export default { } return wsExecutor(execReq); }; - mergedExecutor[Symbol.asyncDispose] = () => + return makeAsyncDisposable(mergedExecutor, () => Promise.all( - Array.from(wsExecutorMap.values()).map(executor => executor[Symbol.asyncDispose]()), - ); - return mergedExecutor; + Array.from(wsExecutorMap.values()).map( + executor => isDisposable(executor) && dispose(executor), + ), + ).then(() => {}), + ); }, } satisfies Transport<'ws', WSTransportOptions>; diff --git a/patches/disposablestack+1.1.6.patch b/patches/disposablestack+1.1.6.patch deleted file mode 100644 index 256420972b531..0000000000000 --- a/patches/disposablestack+1.1.6.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/disposablestack/AsyncDisposableStack/implementation.js b/node_modules/disposablestack/AsyncDisposableStack/implementation.js -index a4d1f6b..0e3a472 100644 ---- a/node_modules/disposablestack/AsyncDisposableStack/implementation.js -+++ b/node_modules/disposablestack/AsyncDisposableStack/implementation.js -@@ -80,7 +80,7 @@ CreateMethodProperty(AsyncDisposableStack.prototype, 'disposeAsync', function di - DisposeResources(SLOT.get(asyncDisposableStack, '[[DisposeCapability]]'), NormalCompletion()) - ), - function (completion) { -- return completion['?'](); // step 5 -+ return completion?.['?'](); // step 5 - } - ); - }); diff --git a/setup-jest.js b/setup-jest.js index 7f31939051b80..8ee6d269ecdf6 100644 --- a/setup-jest.js +++ b/setup-jest.js @@ -1,3 +1,4 @@ -Symbol.dispose ||= Symbol.for('Symbol.dispose'); -Symbol.asyncDispose ||= Symbol.for('Symbol.asyncDispose'); require('json-bigint-patch'); +// Needed only for Jest +const { patchSymbols } = require('@whatwg-node/disposablestack'); +patchSymbols(); diff --git a/yarn.lock b/yarn.lock index 06ca737428d93..88a8d7618979f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5630,6 +5630,7 @@ __metadata: dependencies: "@graphql-mesh/string-interpolation": "npm:0.5.5" "@types/ioredis-mock": "npm:8.2.5" + "@whatwg-node/disposablestack": "npm:^0.0.1" ioredis: "npm:^5.3.2" ioredis-mock: "npm:^8.8.3" peerDependencies: @@ -5799,8 +5800,8 @@ __metadata: "@graphql-tools/stitching-directives": "npm:^3.0.2" "@graphql-tools/utils": "npm:^10.2.3" "@graphql-tools/wrap": "npm:^10.0.5" + "@whatwg-node/disposablestack": "npm:^0.0.1" change-case: "npm:^4.1.2" - disposablestack: "npm:^1.1.6" graphql-yoga: "npm:^5.6.0" tslib: "npm:^2.4.0" peerDependencies: @@ -6077,6 +6078,7 @@ __metadata: peerDependencies: "@graphql-mesh/cross-helpers": ^0.4.4 "@graphql-mesh/types": ^0.99.4 + "@graphql-mesh/utils": ^0.99.4 graphql: "*" tslib: ^2.4.0 languageName: unknown @@ -6413,7 +6415,7 @@ __metadata: "@graphql-tools/federation": "npm:^2.2.1" "@graphql-tools/stitch": "npm:^9.2.10" "@graphql-tools/utils": "npm:^10.2.3" - disposablestack: "npm:^1.1.6" + "@whatwg-node/disposablestack": "npm:^0.0.1" graphql-sse: "npm:^2.5.3" graphql-yoga: "npm:^5.6.0" html-minifier-terser: "npm:7.2.0" @@ -6794,6 +6796,7 @@ __metadata: "@graphql-mesh/cross-helpers": "npm:^0.4.4" "@graphql-mesh/string-interpolation": "npm:^0.5.5" "@graphql-mesh/transport-common": "npm:^0.4.4" + "@graphql-mesh/utils": "npm:^0.99.4" "@graphql-tools/utils": "npm:^10.2.3" "@repeaterjs/repeater": "npm:^3.0.6" "@types/ws": "npm:^8" @@ -6936,6 +6939,7 @@ __metadata: "@graphql-mesh/cross-helpers": "npm:0.4.4" "@graphql-mesh/string-interpolation": "npm:^0.5.5" "@graphql-mesh/transport-common": "npm:^0.4.4" + "@graphql-mesh/utils": "npm:^0.99.4" "@graphql-tools/executor-graphql-ws": "npm:^1.2.0" "@graphql-tools/utils": "npm:^10.2.3" "@types/ws": "npm:^8" @@ -7000,8 +7004,8 @@ __metadata: "@types/js-yaml": "npm:4.0.9" "@types/lodash.topath": "npm:4.5.9" "@types/object-hash": "npm:3.0.6" + "@whatwg-node/disposablestack": "npm:^0.0.1" "@whatwg-node/fetch": "npm:^0.9.13" - disposablestack: "npm:^1.1.6" dset: "npm:^3.1.2" js-yaml: "npm:^4.1.0" lodash.get: "npm:^4.4.2" @@ -12825,6 +12829,15 @@ __metadata: languageName: node linkType: hard +"@whatwg-node/disposablestack@npm:^0.0.1": + version: 0.0.1 + resolution: "@whatwg-node/disposablestack@npm:0.0.1" + dependencies: + tslib: "npm:^2.6.3" + checksum: 10c0/8e1ecd8695bc0ea9630a4daffbaa4b461261e2421ac2ce56f95b63a38835e3d402f3c5f9be00cbb8afdcf85f0cf46105ef1cac1931c2e7ed929660e02ccd1172 + languageName: node + linkType: hard + "@whatwg-node/events@npm:^0.1.0": version: 0.1.1 resolution: "@whatwg-node/events@npm:0.1.1" @@ -17004,7 +17017,7 @@ __metadata: languageName: node linkType: hard -"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.1, define-data-property@npm:^1.1.4": +"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": version: 1.1.4 resolution: "define-data-property@npm:1.1.4" dependencies: @@ -17253,25 +17266,6 @@ __metadata: languageName: node linkType: hard -"disposablestack@npm:^1.1.6": - version: 1.1.6 - resolution: "disposablestack@npm:1.1.6" - dependencies: - call-bind: "npm:^1.0.7" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.3" - es-errors: "npm:^1.3.0" - es-set-tostringtag: "npm:^2.0.3" - get-intrinsic: "npm:^1.2.4" - globalthis: "npm:^1.0.4" - has-symbols: "npm:^1.0.3" - hasown: "npm:^2.0.2" - internal-slot: "npm:^1.0.7" - suppressed-error: "npm:^1.0.3" - checksum: 10c0/ea7e165148f076f3c9e7f40aa8f8deb7d33cd9d09c3938c593a899bdbc43fd1cd746182c0cefb0c54be6ec375a5634e68907408f93a2916bb50c6861f978ae62 - languageName: node - linkType: hard - "dlv@npm:^1.1.3": version: 1.1.3 resolution: "dlv@npm:1.1.3" @@ -17829,7 +17823,7 @@ __metadata: languageName: node linkType: hard -"es-errors@npm:^1.1.0, es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": +"es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": version: 1.3.0 resolution: "es-errors@npm:1.3.0" checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85 @@ -20278,7 +20272,7 @@ __metadata: languageName: node linkType: hard -"globalthis@npm:^1.0.3, globalthis@npm:^1.0.4": +"globalthis@npm:^1.0.3": version: 1.0.4 resolution: "globalthis@npm:1.0.4" dependencies: @@ -20808,7 +20802,7 @@ __metadata: languageName: node linkType: hard -"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.1, has-property-descriptors@npm:^1.0.2": +"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2": version: 1.0.2 resolution: "has-property-descriptors@npm:1.0.2" dependencies: @@ -33357,22 +33351,6 @@ __metadata: languageName: node linkType: hard -"suppressed-error@npm:^1.0.3": - version: 1.0.3 - resolution: "suppressed-error@npm:1.0.3" - dependencies: - define-data-property: "npm:^1.1.1" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.22.3" - es-errors: "npm:^1.1.0" - function-bind: "npm:^1.1.2" - globalthis: "npm:^1.0.3" - has-property-descriptors: "npm:^1.0.1" - set-function-name: "npm:^2.0.1" - checksum: 10c0/e16592004cabda11bc257d437264d657ed2fabf20b6bea0f2b1978fcb6e0c20b09e398c92773c337e21ef856bb247e320e907c734ccd1cae96395823e6d84aef - languageName: node - linkType: hard - "svg-parser@npm:^2.0.2": version: 2.0.4 resolution: "svg-parser@npm:2.0.4"