nitro-graphql 1.6.1 → 1.7.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,298 @@
1
+ import { parse, subscribe, validate } from "graphql";
2
+ import { mergeResolvers, mergeTypeDefs } from "@graphql-tools/merge";
3
+ import { importedConfig } from "#nitro-internal-virtual/graphql-config";
4
+ import { moduleConfig } from "#nitro-internal-virtual/module-config";
5
+ import { directives } from "#nitro-internal-virtual/server-directives";
6
+ import { resolvers } from "#nitro-internal-virtual/server-resolvers";
7
+ import { schemas } from "#nitro-internal-virtual/server-schemas";
8
+ import { makeExecutableSchema } from "@graphql-tools/schema";
9
+ import { defineWebSocketHandler } from "h3";
10
+
11
+ //#region src/routes/graphql-yoga-ws.ts
12
+ const isDev = process.env.NODE_ENV === "development";
13
+ function devLog(message, ...args) {
14
+ if (isDev) console.log(message, ...args);
15
+ }
16
+ const DEFAULT_MAX_SUBSCRIPTIONS_PER_PEER = 20;
17
+ const DEFAULT_PING_INTERVAL_MS = 15e3;
18
+ const DEFAULT_PONG_TIMEOUT_MS = 5e3;
19
+ function sendMessage(peer, message) {
20
+ peer.send(JSON.stringify(message));
21
+ }
22
+ function sendErrorMessage(peer, id, errors) {
23
+ sendMessage(peer, {
24
+ id,
25
+ type: "error",
26
+ payload: errors
27
+ });
28
+ }
29
+ function sendNextMessage(peer, id, payload) {
30
+ sendMessage(peer, {
31
+ id,
32
+ type: "next",
33
+ payload
34
+ });
35
+ }
36
+ function sendCompleteMessage(peer, id) {
37
+ sendMessage(peer, {
38
+ id,
39
+ type: "complete"
40
+ });
41
+ }
42
+ async function handleConnectionInit(peer, payload) {
43
+ if (payload) peer.context.connectionParams = payload;
44
+ const onConnect = importedConfig?.websocket?.onConnect;
45
+ if (onConnect) try {
46
+ const result = await onConnect({
47
+ connectionParams: payload || {},
48
+ headers: Object.fromEntries(peer.request.headers.entries()),
49
+ peerId: peer.id,
50
+ remoteAddress: peer.remoteAddress
51
+ });
52
+ if (result === false) {
53
+ sendMessage(peer, {
54
+ type: "connection_error",
55
+ payload: { message: "Connection rejected by server" }
56
+ });
57
+ peer.close();
58
+ return;
59
+ }
60
+ if (result && typeof result === "object") peer.context.connectionContext = result;
61
+ } catch (error) {
62
+ sendMessage(peer, {
63
+ type: "connection_error",
64
+ payload: { message: error instanceof Error ? error.message : "Connection validation failed" }
65
+ });
66
+ peer.close();
67
+ return;
68
+ }
69
+ sendMessage(peer, { type: "connection_ack" });
70
+ startKeepAlive(peer);
71
+ }
72
+ function handlePing(peer) {
73
+ sendMessage(peer, { type: "pong" });
74
+ }
75
+ function handlePong(peer) {
76
+ if (peer.context.pongTimeout) {
77
+ clearTimeout(peer.context.pongTimeout);
78
+ peer.context.pongTimeout = null;
79
+ }
80
+ }
81
+ function startKeepAlive(peer) {
82
+ stopKeepAlive(peer);
83
+ peer.context.pingInterval = setInterval(() => {
84
+ sendMessage(peer, { type: "ping" });
85
+ peer.context.pongTimeout = setTimeout(() => {
86
+ devLog("[GraphQL WS] Peer did not respond to ping, closing connection");
87
+ peer.close();
88
+ }, DEFAULT_PONG_TIMEOUT_MS);
89
+ }, DEFAULT_PING_INTERVAL_MS);
90
+ }
91
+ function stopKeepAlive(peer) {
92
+ if (peer.context.pingInterval) {
93
+ clearInterval(peer.context.pingInterval);
94
+ peer.context.pingInterval = null;
95
+ }
96
+ if (peer.context.pongTimeout) {
97
+ clearTimeout(peer.context.pongTimeout);
98
+ peer.context.pongTimeout = null;
99
+ }
100
+ }
101
+ async function handleSubscribe(peer, msg, schema) {
102
+ if (!msg.id || !msg.payload) {
103
+ sendErrorMessage(peer, msg.id, [{ message: "Invalid subscribe message" }]);
104
+ return;
105
+ }
106
+ const subscriptions = peer.context.subscriptions;
107
+ const connectionParams = peer.context.connectionParams;
108
+ const connectionContext = peer.context.connectionContext;
109
+ if (subscriptions.has(msg.id)) {
110
+ sendErrorMessage(peer, msg.id, [{ message: `Subscription with ID "${msg.id}" already exists` }]);
111
+ return;
112
+ }
113
+ if (subscriptions.size >= DEFAULT_MAX_SUBSCRIPTIONS_PER_PEER) {
114
+ sendErrorMessage(peer, msg.id, [{ message: `Maximum subscriptions limit (${DEFAULT_MAX_SUBSCRIPTIONS_PER_PEER}) reached` }]);
115
+ return;
116
+ }
117
+ try {
118
+ const { query, variables, operationName } = msg.payload;
119
+ const document = typeof query === "string" ? parse(query) : query;
120
+ const validationErrors = validate(schema, document);
121
+ if (validationErrors.length > 0) {
122
+ sendErrorMessage(peer, msg.id, validationErrors.map((err) => ({
123
+ message: err.message,
124
+ locations: err.locations,
125
+ path: err.path
126
+ })));
127
+ return;
128
+ }
129
+ const result = await subscribe({
130
+ schema,
131
+ document,
132
+ variableValues: variables,
133
+ operationName,
134
+ contextValue: {
135
+ connectionParams,
136
+ ...connectionContext,
137
+ headers: Object.fromEntries(peer.request.headers.entries()),
138
+ authorization: peer.request.headers.get("authorization") || connectionParams?.authorization,
139
+ peerId: peer.id,
140
+ remoteAddress: peer.remoteAddress
141
+ }
142
+ });
143
+ if (Symbol.asyncIterator in result) {
144
+ const abortController = new AbortController();
145
+ const trackedSub = {
146
+ iterator: result,
147
+ abortController
148
+ };
149
+ subscriptions.set(msg.id, trackedSub);
150
+ const iterateSubscription = async () => {
151
+ const subscriptionId = msg.id;
152
+ const signal = abortController.signal;
153
+ try {
154
+ for await (const value of result) {
155
+ if (signal.aborted) break;
156
+ sendNextMessage(peer, subscriptionId, value);
157
+ }
158
+ if (!signal.aborted) sendCompleteMessage(peer, subscriptionId);
159
+ } catch (error) {
160
+ if (signal.aborted) return;
161
+ console.error("[GraphQL WS] Subscription error:", error);
162
+ sendErrorMessage(peer, subscriptionId, [{ message: error instanceof Error ? error.message : "Subscription error" }]);
163
+ } finally {
164
+ subscriptions.delete(subscriptionId);
165
+ }
166
+ };
167
+ iterateSubscription();
168
+ } else {
169
+ sendNextMessage(peer, msg.id, result);
170
+ sendCompleteMessage(peer, msg.id);
171
+ }
172
+ } catch (error) {
173
+ console.error("[GraphQL WS] Operation error:", error);
174
+ sendErrorMessage(peer, msg.id, [{ message: error instanceof Error ? error.message : "Operation failed" }]);
175
+ }
176
+ }
177
+ async function handleComplete(peer, msg) {
178
+ if (!msg.id) return;
179
+ const subscriptions = peer.context.subscriptions;
180
+ const tracked = subscriptions.get(msg.id);
181
+ if (tracked) {
182
+ tracked.abortController.abort();
183
+ if (typeof tracked.iterator.return === "function") try {
184
+ await tracked.iterator.return();
185
+ } catch {}
186
+ subscriptions.delete(msg.id);
187
+ }
188
+ }
189
+ async function cleanupSubscriptions(peer, sendComplete = false) {
190
+ const subscriptions = peer.context.subscriptions;
191
+ if (!subscriptions) return;
192
+ for (const [id, tracked] of subscriptions.entries()) {
193
+ if (sendComplete) try {
194
+ sendCompleteMessage(peer, id);
195
+ } catch {}
196
+ tracked.abortController.abort();
197
+ if (typeof tracked.iterator.return === "function") try {
198
+ await tracked.iterator.return();
199
+ } catch (error) {
200
+ console.error(`[GraphQL WS] Error cleaning up subscription ${id}:`, error);
201
+ }
202
+ }
203
+ subscriptions.clear();
204
+ }
205
+ let buildSubgraphSchema = null;
206
+ async function loadFederationSupport() {
207
+ if (buildSubgraphSchema !== null) return buildSubgraphSchema;
208
+ try {
209
+ buildSubgraphSchema = (await import("@apollo/subgraph")).buildSubgraphSchema;
210
+ } catch {
211
+ buildSubgraphSchema = false;
212
+ }
213
+ return buildSubgraphSchema;
214
+ }
215
+ async function createMergedSchema() {
216
+ const typeDefs = mergeTypeDefs([schemas.map((schema$1) => schema$1.def).join("\n\n")], {
217
+ throwOnConflict: true,
218
+ commentDescriptions: true,
219
+ sort: true
220
+ });
221
+ const mergedResolvers = mergeResolvers(resolvers.map((r) => r.resolver));
222
+ const federationEnabled = moduleConfig.federation?.enabled;
223
+ let schema;
224
+ if (federationEnabled) {
225
+ const buildSubgraph = await loadFederationSupport();
226
+ if (buildSubgraph) schema = buildSubgraph({
227
+ typeDefs: typeof typeDefs === "string" ? parse(typeDefs) : typeDefs,
228
+ resolvers: mergedResolvers
229
+ });
230
+ else {
231
+ console.warn("[GraphQL WS] Federation enabled but @apollo/subgraph not available");
232
+ schema = makeExecutableSchema({
233
+ typeDefs,
234
+ resolvers: mergedResolvers
235
+ });
236
+ }
237
+ } else schema = makeExecutableSchema({
238
+ typeDefs,
239
+ resolvers: mergedResolvers
240
+ });
241
+ if (directives && directives.length > 0) {
242
+ for (const { directive } of directives) if (directive.transformer) schema = directive.transformer(schema);
243
+ }
244
+ return schema;
245
+ }
246
+ let schemaPromise = null;
247
+ async function getSchema() {
248
+ if (!schemaPromise) schemaPromise = createMergedSchema();
249
+ return schemaPromise;
250
+ }
251
+ var graphql_yoga_ws_default = defineWebSocketHandler({
252
+ async open(peer) {
253
+ devLog("[GraphQL WS] Client connected");
254
+ peer.context.subscriptions = /* @__PURE__ */ new Map();
255
+ },
256
+ async message(peer, message) {
257
+ try {
258
+ const data = message.text();
259
+ const msg = JSON.parse(data);
260
+ const currentSchema = await getSchema();
261
+ switch (msg.type) {
262
+ case "connection_init":
263
+ await handleConnectionInit(peer, msg.payload);
264
+ break;
265
+ case "ping":
266
+ handlePing(peer);
267
+ break;
268
+ case "pong":
269
+ handlePong(peer);
270
+ break;
271
+ case "subscribe":
272
+ await handleSubscribe(peer, msg, currentSchema);
273
+ break;
274
+ case "complete":
275
+ await handleComplete(peer, msg);
276
+ break;
277
+ default: devLog("[GraphQL WS] Unknown message type:", msg.type);
278
+ }
279
+ } catch (error) {
280
+ console.error("[GraphQL WS] Message handling error:", error);
281
+ sendMessage(peer, {
282
+ type: "error",
283
+ payload: [{ message: "Invalid message format" }]
284
+ });
285
+ }
286
+ },
287
+ async close(peer, details) {
288
+ devLog("[GraphQL WS] Client disconnected:", details);
289
+ stopKeepAlive(peer);
290
+ await cleanupSubscriptions(peer, true);
291
+ },
292
+ async error(_peer, error) {
293
+ console.error("[GraphQL WS] WebSocket error:", error);
294
+ }
295
+ });
296
+
297
+ //#endregion
298
+ export { graphql_yoga_ws_default as default };
@@ -1,6 +1,6 @@
1
- import * as h33 from "h3";
1
+ import * as h39 from "h3";
2
2
 
3
3
  //#region src/routes/graphql-yoga.d.ts
4
- declare const _default: h33.EventHandler<h33.EventHandlerRequest, Promise<Response>>;
4
+ declare const _default: h39.EventHandler<h39.EventHandlerRequest, Promise<Response>>;
5
5
  //#endregion
6
6
  export { _default as default };
@@ -0,0 +1,146 @@
1
+ //#region src/subscribe/index.d.ts
2
+ /**
3
+ * GraphQL Subscription Client
4
+ * Framework-agnostic subscription client supporting WebSocket and SSE transports
5
+ *
6
+ * Uses native browser WebSocket API and EventSource for maximum compatibility.
7
+ *
8
+ * @example WebSocket (default)
9
+ * ```typescript
10
+ * import { createSubscriptionClient } from 'nitro-graphql/subscribe'
11
+ *
12
+ * const client = createSubscriptionClient({ wsEndpoint: '/api/graphql/ws' })
13
+ *
14
+ * // Simple subscription
15
+ * client.subscribe(
16
+ * 'subscription { countdown(from: 10) }',
17
+ * {},
18
+ * (data) => console.log(data),
19
+ * (error) => console.error(error)
20
+ * )
21
+ *
22
+ * // Multiplexed session (multiple subscriptions, single connection)
23
+ * const session = client.createSession()
24
+ * session.subscribe(query1, vars1, onData1)
25
+ * session.subscribe(query2, vars2, onData2)
26
+ * ```
27
+ *
28
+ * @example SSE Transport (same API, different transport)
29
+ * ```typescript
30
+ * // Use SSE when WebSocket is blocked (corporate firewalls, etc.)
31
+ * client.subscribe(
32
+ * 'subscription { countdown(from: 10) }',
33
+ * {},
34
+ * (data) => console.log(data),
35
+ * (error) => console.error(error),
36
+ * { transport: 'sse' }
37
+ * )
38
+ * ```
39
+ *
40
+ * @example Auto Transport (WebSocket first, SSE fallback)
41
+ * ```typescript
42
+ * // Automatically fallback to SSE if WebSocket fails
43
+ * client.subscribe(
44
+ * 'subscription { countdown(from: 10) }',
45
+ * {},
46
+ * (data) => console.log(data),
47
+ * (error) => console.error(error),
48
+ * { transport: 'auto' }
49
+ * )
50
+ * ```
51
+ *
52
+ * @module nitro-graphql/subscribe
53
+ */
54
+ type ConnectionState = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'error';
55
+ /** Transport type for subscriptions */
56
+ type SubscriptionTransport = 'websocket' | 'sse' | 'auto';
57
+ /** Transport options for subscribe calls */
58
+ interface TransportOptions {
59
+ /** Transport type: 'websocket' (default), 'sse', or 'auto' (WS first, SSE fallback) */
60
+ transport?: SubscriptionTransport;
61
+ /** Shorthand for { transport: 'sse' } */
62
+ sse?: boolean;
63
+ }
64
+ interface SubscriptionOptions<TVariables = Record<string, unknown>> {
65
+ query: string;
66
+ variables?: TVariables;
67
+ onData?: (data: unknown) => void;
68
+ onError?: (error: Error) => void;
69
+ onConnected?: () => void;
70
+ onReconnected?: () => void;
71
+ onDisconnected?: () => void;
72
+ onRetrying?: (attempt: number, maxAttempts: number) => void;
73
+ onMaxRetriesReached?: () => void;
74
+ onStateChange?: (state: ConnectionState) => void;
75
+ maxRetries?: number;
76
+ connectionTimeoutMs?: number;
77
+ connectionParams?: Record<string, unknown>;
78
+ }
79
+ interface SubscriptionHandle {
80
+ unsubscribe: () => void;
81
+ readonly isConnected: boolean;
82
+ readonly state: ConnectionState;
83
+ readonly id: string;
84
+ /** The active transport type */
85
+ readonly transport: 'websocket' | 'sse';
86
+ }
87
+ type StateChangeCallback = (state: ConnectionState, subscriptionCount: number) => void;
88
+ interface SubscriptionSession {
89
+ subscribe: <TData = unknown, TVariables = Record<string, unknown>>(query: string, variables?: TVariables, onData?: (data: TData) => void, onError?: (error: Error) => void) => SubscriptionHandle;
90
+ readonly state: ConnectionState;
91
+ readonly isConnected: boolean;
92
+ readonly subscriptionCount: number;
93
+ close: () => void;
94
+ onStateChange: (callback: StateChangeCallback) => () => void;
95
+ }
96
+ interface SubscriptionClientConfig {
97
+ /** WebSocket endpoint (default: '/api/graphql/ws') */
98
+ wsEndpoint?: string;
99
+ /** SSE endpoint for SSE transport (default: '/api/graphql') */
100
+ sseEndpoint?: string;
101
+ /** Connection parameters for WebSocket handshake */
102
+ connectionParams?: Record<string, unknown> | (() => Record<string, unknown> | Promise<Record<string, unknown>>);
103
+ /** Connection timeout in ms (default: 10000) */
104
+ connectionTimeoutMs?: number;
105
+ /** Maximum retry attempts (default: 5) */
106
+ maxRetries?: number;
107
+ }
108
+ interface SubscriptionClient {
109
+ subscribe: <TData = unknown, TVariables = Record<string, unknown>>(query: string, variables?: TVariables, onData?: (data: TData) => void, onError?: (error: Error) => void, transportOptions?: TransportOptions) => SubscriptionHandle;
110
+ subscribeAsync: <_TData = unknown, TVariables = Record<string, unknown>>(options: SubscriptionOptions<TVariables>, transportOptions?: TransportOptions) => Promise<SubscriptionHandle>;
111
+ createSession: () => SubscriptionSession;
112
+ }
113
+ declare function createSubscriptionClient(config?: SubscriptionClientConfig): SubscriptionClient;
114
+ interface SseSubscriptionOptions<TVariables = Record<string, unknown>> {
115
+ query: string;
116
+ variables?: TVariables;
117
+ onData?: (data: unknown) => void;
118
+ onError?: (error: Error) => void;
119
+ onConnected?: () => void;
120
+ onReconnected?: () => void;
121
+ onDisconnected?: () => void;
122
+ onStateChange?: (state: ConnectionState) => void;
123
+ }
124
+ interface SseSubscriptionHandle {
125
+ close: () => void;
126
+ }
127
+ /**
128
+ * Create an SSE subscription using native EventSource (legacy API)
129
+ * Use this when WebSocket is blocked (corporate firewalls, etc.)
130
+ *
131
+ * @deprecated Use the unified client with { transport: 'sse' } instead
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const handle = createSseSubscription('/api/graphql', {
136
+ * query: 'subscription { countdown(from: 10) }',
137
+ * onData: (data) => console.log(data),
138
+ * })
139
+ *
140
+ * // Later...
141
+ * handle.close()
142
+ * ```
143
+ */
144
+ declare function createSseSubscription<TData = unknown, TVariables = Record<string, unknown>>(endpoint: string, options: SseSubscriptionOptions<TVariables>): SseSubscriptionHandle;
145
+ //#endregion
146
+ export { ConnectionState, SseSubscriptionHandle, SseSubscriptionOptions, StateChangeCallback, SubscriptionClient, SubscriptionClientConfig, SubscriptionHandle, SubscriptionOptions, SubscriptionSession, SubscriptionTransport, TransportOptions, createSseSubscription, createSubscriptionClient };