From e5b12bddad2a9aacf81626123e86e0d92f00f066 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 10 May 2026 11:12:24 +0000 Subject: [PATCH 1/3] fix(codegen): replace graphql-ws type import with inline shims in orm-realtime template The orm-realtime.ts template used `type WsClient = import('graphql-ws').Client` which requires graphql-ws to be resolvable at compile time in every SDK consumer, even though the actual require() is lazy inside the RealtimeManager constructor. Replace with minimal inline type definitions (WsClient, WsClientOptions, WsSink, WsExecutionResult) so generated code compiles without graphql-ws as a dependency. Consumers that never use subscriptions never need the package at all. --- .../core/codegen/templates/orm-realtime.ts | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/graphql/codegen/src/core/codegen/templates/orm-realtime.ts b/graphql/codegen/src/core/codegen/templates/orm-realtime.ts index b9ac8027a..341bd3253 100644 --- a/graphql/codegen/src/core/codegen/templates/orm-realtime.ts +++ b/graphql/codegen/src/core/codegen/templates/orm-realtime.ts @@ -9,9 +9,51 @@ * Any changes here will affect all generated ORM clients. */ -// graphql-ws is loaded lazily so that importing this module does not -// throw when the package is absent (e.g. CLI-only consumers). -type WsClient = import('graphql-ws').Client; +// Minimal type shims for graphql-ws so that this module compiles +// without requiring graphql-ws to be installed. The actual library +// is loaded lazily via require() inside the RealtimeManager +// constructor — consumers that never use subscriptions never need +// the package at all. + +interface WsGraphQLError { + readonly message: string; + readonly [key: string]: unknown; +} + +interface WsExecutionResult> { + data?: TData | null; + errors?: readonly WsGraphQLError[]; + extensions?: Record; +} + +interface WsSink { + next(value: T): void; + error(error: unknown): void; + complete(): void; +} + +interface WsClient { + subscribe>( + payload: { query: string; variables?: Record }, + sink: WsSink>, + ): () => void; + dispose(): void; +} + +interface WsClientOptions { + url: string; + lazy?: boolean; + retryAttempts?: number; + retryWait?: (retryCount: number) => Promise; + connectionParams?: + | Record + | (() => Promise> | Record); + on?: { + connecting?: () => void; + connected?: () => void; + closed?: (event: unknown) => void; + }; +} // ============================================================================ // Types @@ -138,7 +180,9 @@ export class RealtimeManager { constructor(config: RealtimeConfig) { // eslint-disable-next-line @typescript-eslint/no-var-requires - const { createClient: createWsClient } = require('graphql-ws') as typeof import('graphql-ws'); + const { createClient: createWsClient } = require('graphql-ws') as { + createClient: (options: WsClientOptions) => WsClient; + }; const retryWait = async (retryCount: number): Promise => { if (typeof config.retryWait === 'function') { From 2d64fe1e37e1f3ede4c7a327765f79df9187f59b Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 10 May 2026 19:53:47 +0000 Subject: [PATCH 2/3] refactor(codegen): accept WsClient via config instead of require('graphql-ws') Instead of the generated RealtimeManager doing require('graphql-ws') internally (which fails in ESM and forces a hidden dependency), the consumer now passes a WsClient instance via RealtimeConfig.client: import { createClient } from 'graphql-ws'; const orm = createOrmClient({ endpoint: '...', realtime: { client: createClient({ url: 'wss://...' }) }, }); This gives the developer full control over WS connection options, auth, and transport. If subscriptions aren't used, graphql-ws is never imported at all. --- .../src/core/codegen/templates/orm-client.ts | 1 + .../core/codegen/templates/orm-realtime.ts | 138 +++++------------- 2 files changed, 34 insertions(+), 105 deletions(-) diff --git a/graphql/codegen/src/core/codegen/templates/orm-client.ts b/graphql/codegen/src/core/codegen/templates/orm-client.ts index 8dae2f06a..ab9c99ced 100644 --- a/graphql/codegen/src/core/codegen/templates/orm-client.ts +++ b/graphql/codegen/src/core/codegen/templates/orm-client.ts @@ -41,6 +41,7 @@ export type { SubscriptionFieldMeta, SubscriptionOperation, Unsubscribe, + WsClient, } from './realtime'; export { RealtimeManager } from './realtime'; diff --git a/graphql/codegen/src/core/codegen/templates/orm-realtime.ts b/graphql/codegen/src/core/codegen/templates/orm-realtime.ts index 341bd3253..38f6b94fa 100644 --- a/graphql/codegen/src/core/codegen/templates/orm-realtime.ts +++ b/graphql/codegen/src/core/codegen/templates/orm-realtime.ts @@ -9,11 +9,9 @@ * Any changes here will affect all generated ORM clients. */ -// Minimal type shims for graphql-ws so that this module compiles -// without requiring graphql-ws to be installed. The actual library -// is loaded lazily via require() inside the RealtimeManager -// constructor — consumers that never use subscriptions never need -// the package at all. +// Minimal type shims so this module compiles without graphql-ws +// installed. Consumers supply a WsClient via RealtimeConfig; +// the SDK itself never imports or requires graphql-ws. interface WsGraphQLError { readonly message: string; @@ -32,7 +30,11 @@ interface WsSink { complete(): void; } -interface WsClient { +/** + * Minimal interface matching the graphql-ws Client. + * Consumers pass a concrete instance via RealtimeConfig.client. + */ +export interface WsClient { subscribe>( payload: { query: string; variables?: Record }, sink: WsSink>, @@ -40,21 +42,6 @@ interface WsClient { dispose(): void; } -interface WsClientOptions { - url: string; - lazy?: boolean; - retryAttempts?: number; - retryWait?: (retryCount: number) => Promise; - connectionParams?: - | Record - | (() => Promise> | Record); - on?: { - connecting?: () => void; - connected?: () => void; - closed?: (event: unknown) => void; - }; -} - // ============================================================================ // Types // ============================================================================ @@ -127,40 +114,32 @@ export interface SubscriptionFieldMeta { /** * Configuration for the realtime (WebSocket) connection. * Pass this as the `realtime` option in OrmClientConfig. + * + * @example + * ```ts + * import { createClient } from 'graphql-ws'; + * + * const client = createOrmClient({ + * endpoint: 'https://api.example.com/graphql', + * realtime: { + * client: createClient({ url: 'wss://api.example.com/graphql' }), + * }, + * }); + * ``` */ export interface RealtimeConfig { - /** WebSocket endpoint URL (e.g., 'wss://api.example.com/graphql') */ - url: string; - /** - * Returns the current auth token. Called on connection init and - * on reconnection so the client always sends a fresh token. - */ - getToken?: () => string | Promise; - /** - * Additional connection parameters sent during WebSocket handshake. - * Merged with the authorization header from getToken(). - */ - connectionParams?: Record; - /** - * Whether to connect lazily (on first subscribe) or eagerly. - * @default true - */ - lazy?: boolean; /** - * Maximum number of reconnection attempts before giving up. - * @default 5 + * A graphql-ws Client instance (or any object satisfying WsClient). + * The consumer creates this themselves, giving full control over + * connection options, auth, and transport. + * + * @example + * ```ts + * import { createClient } from 'graphql-ws'; + * const wsClient = createClient({ url: 'wss://...' }); + * ``` */ - retryAttempts?: number; - /** - * Delay between reconnection attempts in milliseconds, - * or a function for custom backoff. - * @default 1000 - */ - retryWait?: number | ((retryCount: number) => number | Promise); - /** Called when the WebSocket connection is established */ - onConnected?: () => void; - /** Called when the WebSocket connection is closed */ - onDisconnected?: (reason?: unknown) => void; + client: WsClient; } // ============================================================================ @@ -168,8 +147,8 @@ export interface RealtimeConfig { // ============================================================================ /** - * Manages a single graphql-ws WebSocket connection and multiplexes - * subscriptions over it. Created lazily by OrmClient when `realtime` + * Manages a graphql-ws WebSocket client and multiplexes + * subscriptions over it. Created by OrmClient when `realtime` * config is provided. */ export class RealtimeManager { @@ -179,58 +158,7 @@ export class RealtimeManager { private activeSubscriptions = 0; constructor(config: RealtimeConfig) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { createClient: createWsClient } = require('graphql-ws') as { - createClient: (options: WsClientOptions) => WsClient; - }; - - const retryWait = async (retryCount: number): Promise => { - if (typeof config.retryWait === 'function') { - const result = config.retryWait(retryCount); - const ms = typeof result === 'number' ? result : await result; - await new Promise((resolve) => setTimeout(resolve, ms)); - } else { - const base = - typeof config.retryWait === 'number' ? config.retryWait : 1000; - await new Promise((resolve) => - setTimeout(resolve, base * Math.pow(2, retryCount)), - ); - } - }; - - this.wsClient = createWsClient({ - url: config.url, - lazy: config.lazy ?? true, - retryAttempts: config.retryAttempts ?? 5, - retryWait, - connectionParams: async () => { - const params: Record = { - ...config.connectionParams, - }; - if (config.getToken) { - const token = await config.getToken(); - params['authorization'] = `Bearer ${token}`; - } - return params; - }, - on: { - connecting: () => { - const newState = - this.connectionState === 'disconnected' - ? 'connecting' - : 'reconnecting'; - this.setConnectionState(newState); - }, - connected: () => { - this.setConnectionState('connected'); - config.onConnected?.(); - }, - closed: (event) => { - this.setConnectionState('disconnected'); - config.onDisconnected?.(event); - }, - }, - }); + this.wsClient = config.client; } /** From 5fb70207605c613ee0c45102c9d5a2f6396202d5 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 10 May 2026 19:57:39 +0000 Subject: [PATCH 3/3] test: update snapshot for WsClient export --- .../codegen/__snapshots__/client-generator.test.ts.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap index ec4ad6d1d..a088b8860 100644 --- a/graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap +++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/client-generator.test.ts.snap @@ -137,6 +137,7 @@ export type { SubscriptionFieldMeta, SubscriptionOperation, Unsubscribe, + WsClient, } from './realtime'; export { RealtimeManager } from './realtime';