@fluojs/graphql 1.0.4 → 2.0.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.
Files changed (39) hide show
  1. package/README.ko.md +153 -17
  2. package/README.md +158 -17
  3. package/dist/dataloader/dataloader.d.ts.map +1 -1
  4. package/dist/dataloader/dataloader.js +2 -3
  5. package/dist/decorators.d.ts +53 -0
  6. package/dist/decorators.d.ts.map +1 -1
  7. package/dist/decorators.js +123 -1
  8. package/dist/discovery.d.ts.map +1 -1
  9. package/dist/discovery.js +13 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/instance-of-patch.d.ts +21 -0
  13. package/dist/instance-of-patch.d.ts.map +1 -0
  14. package/dist/instance-of-patch.js +126 -0
  15. package/dist/metadata.d.ts +21 -1
  16. package/dist/metadata.d.ts.map +1 -1
  17. package/dist/metadata.js +68 -7
  18. package/dist/module.d.ts +14 -10
  19. package/dist/module.d.ts.map +1 -1
  20. package/dist/module.js +69 -14
  21. package/dist/node/graphql-websocket-transport.d.ts +53 -0
  22. package/dist/node/graphql-websocket-transport.d.ts.map +1 -0
  23. package/dist/node/graphql-websocket-transport.js +219 -0
  24. package/dist/pipeline/input-pipeline.js +1 -1
  25. package/dist/schema/object-field-resolvers.d.ts +18 -0
  26. package/dist/schema/object-field-resolvers.d.ts.map +1 -0
  27. package/dist/schema/object-field-resolvers.js +86 -0
  28. package/dist/schema/schema.d.ts +5 -1
  29. package/dist/schema/schema.d.ts.map +1 -1
  30. package/dist/schema/schema.js +75 -39
  31. package/dist/service.d.ts +10 -12
  32. package/dist/service.d.ts.map +1 -1
  33. package/dist/service.js +191 -303
  34. package/dist/transport/transport.d.ts.map +1 -1
  35. package/dist/transport/transport.js +1 -1
  36. package/dist/types.d.ts +40 -3
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/types.js +19 -1
  39. package/package.json +11 -10
package/dist/service.js CHANGED
@@ -4,27 +4,35 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
4
4
  function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
5
5
  function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
6
6
  function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
7
- import { Controller, Get, Post } from '@fluojs/http';
7
+ import { createRequire } from 'node:module';
8
8
  import { Inject } from '@fluojs/core';
9
+ import { Controller, Get, Post } from '@fluojs/http';
9
10
  import { APPLICATION_LOGGER, COMPILED_MODULES, HTTP_APPLICATION_ADAPTER, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
10
11
  import { discoverResolverDescriptors } from './discovery.js';
11
12
  import { createGraphqlValidationPlugin, resolveGraphqlRequestLimits } from './guardrails.js';
13
+ import { installGraphqlInstanceOfPatch } from './instance-of-patch.js';
12
14
  import { GRAPHQL_INTERNAL_MODULE_OPTIONS_TOKEN } from './internal-tokens.js';
13
15
  import { createCodeFirstSchema, resolveSchema } from './schema/schema.js';
14
16
  import { isGraphqlPath, toFetchRequest, writeFetchResponse } from './transport/transport.js';
15
17
  import { GRAPHQL_OPERATION_CONTAINER } from './types.js';
16
18
  const GRAPHQL_CONTEXT_OVERRIDE = Symbol('fluo.graphql.context.override');
19
+ const runtimeRequire = createRequire(import.meta.url);
17
20
  const DEFAULT_GRAPHQL_WEBSOCKET_LIMITS = {
18
21
  maxConnections: 100,
19
22
  maxOperationsPerConnection: 25,
20
23
  maxPayloadBytes: 64 * 1024
21
24
  };
22
- function hasNodeUpgradeServer(value) {
23
- if (typeof value !== 'object' || value === null) {
24
- return false;
25
+ function collectCleanupError(errors, error) {
26
+ if (error instanceof AggregateError) {
27
+ for (const nested of error.errors) {
28
+ collectCleanupError(errors, nested);
29
+ }
30
+ return;
31
+ }
32
+ if (errors.includes(error)) {
33
+ return;
25
34
  }
26
- const server = value;
27
- return typeof server.on === 'function' && typeof server.off === 'function';
35
+ errors.push(error);
28
36
  }
29
37
  function buildFrameworkRequestFromFetchRequest(request) {
30
38
  const requestUrl = new URL(request.url);
@@ -40,45 +48,10 @@ function buildFrameworkRequestFromFetchRequest(request) {
40
48
  url: requestUrl.pathname + requestUrl.search
41
49
  };
42
50
  }
43
- function buildFrameworkRequestFromIncomingMessage(request) {
44
- const requestUrl = new URL(request.url ?? '/graphql', 'http://localhost');
45
- return {
46
- cookies: {},
47
- headers: request.headers,
48
- method: request.method ?? 'GET',
49
- params: {},
50
- path: requestUrl.pathname,
51
- query: Object.fromEntries(requestUrl.searchParams.entries()),
52
- raw: request,
53
- url: requestUrl.pathname + requestUrl.search
54
- };
55
- }
56
- function isConnectionParamsRecord(value) {
57
- return typeof value === 'object' && value !== null && !Array.isArray(value);
58
- }
59
- function closeWebSocketServer(server) {
60
- return new Promise((resolve, reject) => {
61
- server.close(error => {
62
- if (error?.message === 'The server is not running') {
63
- resolve();
64
- return;
65
- }
66
- if (error) {
67
- reject(error);
68
- return;
69
- }
70
- resolve();
71
- });
72
- });
73
- }
74
- let graphqlInstanceOfPatchRefCount = 0;
75
- let restoreGraphqlInstanceOfPatch;
76
- const allowedCrossRealmGraphqlObjects = new WeakSet();
77
-
51
+ let _GraphqlEndpointContr;
78
52
  /**
79
53
  * Declares the HTTP endpoints that receive GraphQL GET and POST requests.
80
54
  */
81
- let _GraphqlEndpointContr;
82
55
  class GraphqlEndpointController {
83
56
  static {
84
57
  ({
@@ -100,22 +73,7 @@ class GraphqlEndpointController {
100
73
  }
101
74
  }
102
75
  export { _GraphqlEndpointContr as GraphqlEndpointController };
103
- function getCrossRealmGraphqlTag(value, constructor) {
104
- const className = constructor.prototype?.[Symbol.toStringTag];
105
- if (typeof className !== 'string' || !className.startsWith('GraphQL')) {
106
- return undefined;
107
- }
108
- if (typeof value !== 'object' || value === null) {
109
- return undefined;
110
- }
111
- if (Symbol.toStringTag in value) {
112
- const valueClassName = value[Symbol.toStringTag];
113
- return valueClassName === className ? className : undefined;
114
- }
115
- const valueClassName = value.constructor?.name;
116
- return valueClassName === className ? className : undefined;
117
- }
118
- function markAllowedCrossRealmGraphqlObjects(value, visited = new WeakSet()) {
76
+ function markAllowedCrossRealmGraphqlObjects(value, allowedObjects, visited = new WeakSet()) {
119
77
  if (typeof value !== 'object' || value === null) {
120
78
  return;
121
79
  }
@@ -124,73 +82,26 @@ function markAllowedCrossRealmGraphqlObjects(value, visited = new WeakSet()) {
124
82
  }
125
83
  visited.add(value);
126
84
  const tag = value[Symbol.toStringTag];
127
- if (typeof tag === 'string' && tag.startsWith('GraphQL')) {
128
- allowedCrossRealmGraphqlObjects.add(value);
85
+ const constructorName = value.constructor?.name;
86
+ if (typeof tag === 'string' && tag.startsWith('GraphQL') || typeof constructorName === 'string' && constructorName.startsWith('GraphQL')) {
87
+ allowedObjects.add(value);
129
88
  }
130
89
  if (Array.isArray(value)) {
131
90
  for (const item of value) {
132
- markAllowedCrossRealmGraphqlObjects(item, visited);
91
+ markAllowedCrossRealmGraphqlObjects(item, allowedObjects, visited);
133
92
  }
134
93
  return;
135
94
  }
136
95
  for (const nestedValue of Object.values(value)) {
137
- markAllowedCrossRealmGraphqlObjects(nestedValue, visited);
96
+ markAllowedCrossRealmGraphqlObjects(nestedValue, allowedObjects, visited);
138
97
  }
139
98
  }
140
- function isAllowedCrossRealmGraphqlObject(value, constructor) {
141
- if (typeof value !== 'object' || value === null) {
142
- return false;
143
- }
144
- return allowedCrossRealmGraphqlObjects.has(value) && getCrossRealmGraphqlTag(value, constructor) !== undefined;
145
- }
146
- function installGraphqlInstanceOfPatch(instanceOfModule) {
147
- if (restoreGraphqlInstanceOfPatch) {
148
- graphqlInstanceOfPatchRefCount += 1;
149
- return releaseGraphqlInstanceOfPatch;
150
- }
151
- const patchedFrom = instanceOfModule.instanceOf;
152
- const patchedInstanceOf = (value, constructor) => {
153
- try {
154
- if (patchedFrom(value, constructor)) {
155
- return true;
156
- }
157
- } catch (error) {
158
- if (isAllowedCrossRealmGraphqlObject(value, constructor)) {
159
- return true;
160
- }
161
- throw error;
162
- }
163
- return isAllowedCrossRealmGraphqlObject(value, constructor);
164
- };
165
- instanceOfModule.instanceOf = patchedInstanceOf;
166
- graphqlInstanceOfPatchRefCount = 1;
167
- restoreGraphqlInstanceOfPatch = () => {
168
- if (instanceOfModule.instanceOf !== patchedInstanceOf) {
169
- return;
170
- }
171
- instanceOfModule.instanceOf = patchedFrom;
172
- };
173
- return releaseGraphqlInstanceOfPatch;
174
- }
175
- function releaseGraphqlInstanceOfPatch() {
176
- if (graphqlInstanceOfPatchRefCount === 0) {
177
- return;
178
- }
179
- graphqlInstanceOfPatchRefCount -= 1;
180
- if (graphqlInstanceOfPatchRefCount > 0) {
181
- return;
182
- }
183
- restoreGraphqlInstanceOfPatch?.();
184
- restoreGraphqlInstanceOfPatch = undefined;
185
- }
186
99
  async function loadGraphqlDeps() {
187
- const {
188
- createRequire
189
- } = await import('node:module');
190
- const runtimeRequire = createRequire(import.meta.url);
191
- const graphqlMod = runtimeRequire('graphql');
192
- const yogaMod = runtimeRequire('graphql-yoga');
193
- const instanceOfModule = runtimeRequire('graphql/jsutils/instanceOf.js');
100
+ const graphqlSpecifier = 'graphql';
101
+ const yogaSpecifier = 'graphql-yoga';
102
+ const instanceOfSpecifier = 'graphql/jsutils/instanceOf.js';
103
+ const [graphqlMod, yogaMod] = await Promise.all([import(/* @vite-ignore */graphqlSpecifier), import(/* @vite-ignore */yogaSpecifier)]);
104
+ const instanceOfModule = runtimeRequire(instanceOfSpecifier);
194
105
  return {
195
106
  GraphQLError: graphqlMod.GraphQLError,
196
107
  GraphQLBoolean: graphqlMod.GraphQLBoolean,
@@ -198,11 +109,13 @@ async function loadGraphqlDeps() {
198
109
  GraphQLID: graphqlMod.GraphQLID,
199
110
  GraphQLInt: graphqlMod.GraphQLInt,
200
111
  GraphQLList: graphqlMod.GraphQLList,
112
+ GraphQLNonNull: graphqlMod.GraphQLNonNull,
201
113
  GraphQLObjectType: graphqlMod.GraphQLObjectType,
202
114
  GraphQLSchema: graphqlMod.GraphQLSchema,
203
115
  GraphQLString: graphqlMod.GraphQLString,
204
116
  GraphQLUnionType: graphqlMod.GraphQLUnionType,
205
117
  buildSchema: graphqlMod.buildSchema,
118
+ createGraphQLError: yogaMod.createGraphQLError,
206
119
  createYoga: yogaMod.createYoga,
207
120
  execute: graphqlMod.execute,
208
121
  instanceOfModule,
@@ -218,62 +131,60 @@ class GraphqlLifecycleService {
218
131
  static {
219
132
  [_GraphqlLifecycleServ, _initClass2] = _applyDecs(this, [Inject(RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, HTTP_APPLICATION_ADAPTER, GRAPHQL_INTERNAL_MODULE_OPTIONS_TOKEN)], []).c;
220
133
  }
134
+ allowedCrossRealmGraphqlObjects;
221
135
  graphQLErrorConstructor;
222
- middlewareRegistered = false;
223
- operationContainers = new WeakMap();
136
+ operationContainers = new Map();
224
137
  requestContexts = new WeakMap();
138
+ failedWebsocketOperationContainers = new Map();
139
+ websocketContainerDisposals = new Map();
140
+ websocketContainersAttemptedInCurrentCleanup;
225
141
  websocketOperationContainers = new Map();
226
- websocketDisposable;
227
- websocketServer;
228
- websocketUpgradeListener;
229
- websocketUpgradeServer;
142
+ websocketTransport;
230
143
  executeGraphqlOperation;
231
144
  releaseGraphqlInstanceOfPatch;
232
145
  subscribeGraphqlOperation;
233
146
  yoga;
234
- middleware = {
235
- handle: async (context, next) => {
236
- if (!isGraphqlPath(context.request.path)) {
237
- await next();
238
- return;
147
+ async handle(context, next) {
148
+ if (!isGraphqlPath(context.request.path)) {
149
+ await next();
150
+ return;
151
+ }
152
+ const yoga = this.yoga;
153
+ if (!yoga) {
154
+ this.logger.error('GraphQL middleware was invoked before GraphQL Yoga initialization.', undefined, 'GraphqlLifecycleService');
155
+ context.response.setStatus(500);
156
+ await context.response.send({
157
+ errors: [{
158
+ message: 'GraphQL server not initialized.'
159
+ }]
160
+ });
161
+ return;
162
+ }
163
+ try {
164
+ const fetchRequest = toFetchRequest(context.request);
165
+ this.requestContexts.set(fetchRequest, {
166
+ principal: context.requestContext.principal,
167
+ request: context.request
168
+ });
169
+ try {
170
+ const fetchResponse = await yoga.fetch(fetchRequest);
171
+ await writeFetchResponse(fetchResponse, context.response);
172
+ } finally {
173
+ this.requestContexts.delete(fetchRequest);
174
+ await this.disposeOperationContainer(fetchRequest);
239
175
  }
240
- const yoga = this.yoga;
241
- if (!yoga) {
242
- this.logger.error('GraphQL middleware was invoked before GraphQL Yoga initialization.', undefined, 'GraphqlLifecycleService');
176
+ } catch (error) {
177
+ this.logger.error('Failed to process GraphQL request.', error, 'GraphqlLifecycleService');
178
+ if (!context.response.committed) {
243
179
  context.response.setStatus(500);
244
180
  await context.response.send({
245
181
  errors: [{
246
- message: 'GraphQL server not initialized.'
182
+ message: 'Internal server error.'
247
183
  }]
248
184
  });
249
- return;
250
- }
251
- try {
252
- const fetchRequest = toFetchRequest(context.request);
253
- this.requestContexts.set(fetchRequest, {
254
- principal: context.requestContext.principal,
255
- request: context.request
256
- });
257
- try {
258
- const fetchResponse = await yoga.fetch(fetchRequest);
259
- await writeFetchResponse(fetchResponse, context.response);
260
- } finally {
261
- this.requestContexts.delete(fetchRequest);
262
- await this.disposeOperationContainer(fetchRequest);
263
- }
264
- } catch (error) {
265
- this.logger.error('Failed to process GraphQL request.', error, 'GraphqlLifecycleService');
266
- if (!context.response.committed) {
267
- context.response.setStatus(500);
268
- await context.response.send({
269
- errors: [{
270
- message: 'Internal server error.'
271
- }]
272
- });
273
- }
274
185
  }
275
186
  }
276
- };
187
+ }
277
188
  constructor(runtimeContainer, compiledModules, logger, adapter, options) {
278
189
  this.runtimeContainer = runtimeContainer;
279
190
  this.compiledModules = compiledModules;
@@ -282,7 +193,7 @@ class GraphqlLifecycleService {
282
193
  this.options = options;
283
194
  }
284
195
  async onApplicationBootstrap() {
285
- if (this.middlewareRegistered) {
196
+ if (this.yoga) {
286
197
  return;
287
198
  }
288
199
  try {
@@ -306,8 +217,6 @@ class GraphqlLifecycleService {
306
217
  schema
307
218
  });
308
219
  await this.registerWebSocketTransport();
309
- this.registerMiddleware();
310
- this.middlewareRegistered = true;
311
220
  } catch (error) {
312
221
  try {
313
222
  await this.resetGraphqlRuntimeState();
@@ -321,13 +230,28 @@ class GraphqlLifecycleService {
321
230
  await this.resetGraphqlRuntimeState();
322
231
  }
323
232
  async resetGraphqlRuntimeState() {
324
- await this.unregisterWebSocketTransport();
325
- this.unregisterMiddleware();
326
- this.middlewareRegistered = false;
233
+ const cleanupErrors = [];
234
+ this.websocketContainersAttemptedInCurrentCleanup = new Set();
235
+ try {
236
+ await this.unregisterWebSocketTransport();
237
+ } catch (error) {
238
+ collectCleanupError(cleanupErrors, error);
239
+ } finally {
240
+ this.websocketContainersAttemptedInCurrentCleanup = undefined;
241
+ }
242
+ try {
243
+ await this.disposeAllOperationContainers();
244
+ } catch (error) {
245
+ collectCleanupError(cleanupErrors, error);
246
+ }
247
+ if (cleanupErrors.length > 0) {
248
+ throw new AggregateError(cleanupErrors, 'Failed to clean up GraphQL runtime resources.');
249
+ }
327
250
  this.executeGraphqlOperation = undefined;
328
251
  this.graphQLErrorConstructor = undefined;
329
252
  this.releaseGraphqlInstanceOfPatch?.();
330
253
  this.releaseGraphqlInstanceOfPatch = undefined;
254
+ this.allowedCrossRealmGraphqlObjects = undefined;
331
255
  this.subscribeGraphqlOperation = undefined;
332
256
  this.yoga = undefined;
333
257
  }
@@ -349,43 +273,18 @@ class GraphqlLifecycleService {
349
273
  };
350
274
  }
351
275
  resolveSchema(deps) {
352
- this.releaseGraphqlInstanceOfPatch ??= installGraphqlInstanceOfPatch(deps.instanceOfModule);
353
- return resolveSchema(deps, this.options.schema, () => this.createCodeFirstSchema(deps), markAllowedCrossRealmGraphqlObjects);
276
+ const allowedObjects = this.allowedCrossRealmGraphqlObjects ?? new WeakSet();
277
+ this.allowedCrossRealmGraphqlObjects = allowedObjects;
278
+ this.releaseGraphqlInstanceOfPatch ??= installGraphqlInstanceOfPatch(deps.instanceOfModule, allowedObjects);
279
+ const markAllowedObject = value => markAllowedCrossRealmGraphqlObjects(value, allowedObjects);
280
+ return resolveSchema(deps, this.options.schema, () => this.createCodeFirstSchema(deps, markAllowedObject), markAllowedObject);
354
281
  }
355
- createCodeFirstSchema(deps) {
356
- return createCodeFirstSchema(deps, this.runtimeContainer, this.discoverResolverDescriptors(), markAllowedCrossRealmGraphqlObjects);
282
+ createCodeFirstSchema(deps, markAllowedObject) {
283
+ return createCodeFirstSchema(deps, this.runtimeContainer, this.discoverResolverDescriptors(), markAllowedObject);
357
284
  }
358
285
  discoverResolverDescriptors() {
359
286
  return discoverResolverDescriptors(this.compiledModules, this.options);
360
287
  }
361
- registerMiddleware() {
362
- for (const compiledModule of this.compiledModules) {
363
- if (!compiledModule.providerTokens.has(_GraphqlLifecycleServ)) {
364
- continue;
365
- }
366
- const middleware = compiledModule.definition.middleware ?? [];
367
- if (!middleware.includes(this.middleware)) {
368
- compiledModule.definition.middleware = [...middleware, this.middleware];
369
- continue;
370
- }
371
- compiledModule.definition.middleware = [...middleware];
372
- }
373
- }
374
- unregisterMiddleware() {
375
- for (const compiledModule of this.compiledModules) {
376
- if (!compiledModule.providerTokens.has(_GraphqlLifecycleServ)) {
377
- continue;
378
- }
379
- const middleware = compiledModule.definition.middleware ?? [];
380
- const remaining = [];
381
- for (const entry of middleware) {
382
- if (entry !== this.middleware) {
383
- remaining.push(entry);
384
- }
385
- }
386
- compiledModule.definition.middleware = remaining;
387
- }
388
- }
389
288
  buildGraphqlContext(request, requestContextOverride, operationContainerOverride) {
390
289
  const storedContext = this.requestContexts.get(request);
391
290
  const requestContext = {
@@ -406,126 +305,81 @@ class GraphqlLifecycleService {
406
305
  };
407
306
  }
408
307
  async registerWebSocketTransport() {
409
- if (!this.isWebSocketTransportEnabled() || this.yoga === undefined || this.websocketUpgradeListener !== undefined) {
308
+ if (!this.isWebSocketTransportEnabled() || this.yoga === undefined || this.websocketTransport !== undefined) {
410
309
  return;
411
310
  }
412
- const [{
413
- handleProtocols
414
- }, {
415
- useServer
416
- }, {
417
- WebSocketServer
418
- }] = await Promise.all([import('graphql-ws'), import('graphql-ws/lib/use/ws'), import('ws')]);
419
- const upgradeServer = this.resolveUpgradeServer();
420
- const websocketLimits = this.resolveWebSocketLimits();
421
- const websocketServer = new WebSocketServer({
422
- handleProtocols: protocols => handleProtocols(protocols),
423
- maxPayload: websocketLimits?.maxPayloadBytes ?? 0,
424
- noServer: true
425
- });
426
- const upgradeListener = (request, socket, head) => {
427
- const targetPath = new URL(request.url ?? '/', 'http://localhost').pathname;
428
- if (!isGraphqlPath(targetPath)) {
429
- return;
430
- }
431
- if (websocketLimits && websocketServer.clients.size >= websocketLimits.maxConnections) {
432
- this.rejectWebSocketUpgrade(socket, 503, 'GraphQL websocket connection count exceeds the configured limit.');
433
- return;
434
- }
435
- websocketServer.handleUpgrade(request, socket, head, websocket => {
436
- websocketServer.emit('connection', websocket, request);
437
- });
438
- };
439
- const websocketDisposable = useServer({
440
- connectionInitWaitTimeout: this.options.subscriptions?.websocket?.connectionInitWaitTimeoutMs,
311
+ const {
312
+ createNodeGraphqlWebSocketTransport
313
+ } = await import('./node/graphql-websocket-transport.js');
314
+ this.websocketTransport = await createNodeGraphqlWebSocketTransport({
315
+ adapter: this.adapter,
316
+ connectionInitWaitTimeoutMs: this.options.subscriptions?.websocket?.connectionInitWaitTimeoutMs,
441
317
  execute: args => {
442
318
  if (!this.executeGraphqlOperation) {
443
319
  throw new Error('GraphQL execute function not initialized.');
444
320
  }
445
321
  return this.executeGraphqlOperation(args);
446
322
  },
447
- onComplete: async (context, message) => {
448
- await this.disposeWebSocketOperationContainer(context.extra.socket, message.id);
323
+ keepAliveMs: this.options.subscriptions?.websocket?.keepAliveMs,
324
+ limits: this.resolveWebSocketLimits(),
325
+ onComplete: async (socketKey, operationId) => {
326
+ await this.disposeWebSocketOperationContainer(socketKey, operationId);
449
327
  },
450
- onDisconnect: async context => {
451
- await this.disposeAllWebSocketOperationContainers(context.extra.socket);
452
- },
453
- onSubscribe: async (context, message) => this.handleWebSocketSubscribe(context, message.id, message.payload),
328
+ onDisconnect: socketKey => this.disposeAllWebSocketOperationContainers(socketKey),
329
+ onSubscribe: request => this.handleWebSocketSubscribe(request),
454
330
  subscribe: args => {
455
331
  if (!this.subscribeGraphqlOperation) {
456
332
  throw new Error('GraphQL subscribe function not initialized.');
457
333
  }
458
334
  return this.subscribeGraphqlOperation(args);
459
335
  }
460
- }, websocketServer, this.options.subscriptions?.websocket?.keepAliveMs);
461
- upgradeServer.on('upgrade', upgradeListener);
462
- this.websocketDisposable = websocketDisposable;
463
- this.websocketServer = websocketServer;
464
- this.websocketUpgradeListener = upgradeListener;
465
- this.websocketUpgradeServer = upgradeServer;
336
+ });
466
337
  }
467
338
  async unregisterWebSocketTransport() {
468
- if (this.websocketUpgradeListener && this.websocketUpgradeServer) {
469
- this.websocketUpgradeServer.off('upgrade', this.websocketUpgradeListener);
470
- }
471
- this.websocketUpgradeListener = undefined;
472
- this.websocketUpgradeServer = undefined;
473
- if (this.websocketServer) {
474
- for (const client of this.websocketServer.clients) {
475
- client.terminate();
476
- }
477
- }
478
- if (this.websocketDisposable) {
339
+ const cleanupErrors = [];
340
+ if (this.websocketTransport) {
479
341
  try {
480
- await this.websocketDisposable.dispose();
342
+ await this.websocketTransport.dispose();
481
343
  } catch (error) {
482
344
  this.logger.error('Failed to dispose GraphQL websocket transport.', error, 'GraphqlLifecycleService');
345
+ collectCleanupError(cleanupErrors, error);
346
+ }
347
+ if (cleanupErrors.length === 0) {
348
+ this.websocketTransport = undefined;
483
349
  }
484
350
  }
485
- this.websocketDisposable = undefined;
486
- if (this.websocketServer) {
351
+ const socketKeys = new Set([...this.websocketOperationContainers.keys(), ...this.failedWebsocketOperationContainers.keys()]);
352
+ for (const socketKey of socketKeys) {
487
353
  try {
488
- await closeWebSocketServer(this.websocketServer);
354
+ await this.disposeAllWebSocketOperationContainers(socketKey);
489
355
  } catch (error) {
490
- this.logger.error('Failed to close GraphQL websocket server.', error, 'GraphqlLifecycleService');
356
+ collectCleanupError(cleanupErrors, error);
491
357
  }
492
358
  }
493
- this.websocketServer = undefined;
494
- for (const socketKey of this.websocketOperationContainers.keys()) {
495
- await this.disposeAllWebSocketOperationContainers(socketKey);
359
+ if (cleanupErrors.length > 0) {
360
+ throw new AggregateError(cleanupErrors, 'Failed to dispose GraphQL websocket resources.');
496
361
  }
497
362
  }
498
363
  isWebSocketTransportEnabled() {
499
364
  return this.options.subscriptions?.websocket?.enabled === true;
500
365
  }
501
- resolveUpgradeServer() {
502
- if (typeof this.adapter.getServer !== 'function') {
503
- throw new Error('GraphQL websocket subscriptions require an HTTP adapter with getServer(). Use the Node HTTP adapter or provide a compatible adapter implementation.');
504
- }
505
- const server = this.adapter.getServer();
506
- if (!hasNodeUpgradeServer(server)) {
507
- throw new Error('GraphQL websocket subscriptions require adapter.getServer() to return a Node HTTP/S server that supports upgrade listeners.');
508
- }
509
- return server;
510
- }
511
- async handleWebSocketSubscribe(context, operationId, payload) {
366
+ async handleWebSocketSubscribe(request) {
512
367
  const yoga = this.yoga;
513
368
  if (!yoga) {
514
369
  throw new Error('GraphQL server not initialized.');
515
370
  }
516
- const websocketLimitError = this.createWebSocketOperationLimitError(context.extra.socket, operationId);
371
+ const websocketLimitError = this.createWebSocketOperationLimitError(request.socket, request.operationId);
517
372
  if (websocketLimitError) {
518
373
  return [websocketLimitError];
519
374
  }
520
- const frameworkRequest = buildFrameworkRequestFromIncomingMessage(context.extra.request);
521
- const fetchRequest = toFetchRequest(frameworkRequest);
522
- const operationContainer = this.getOrCreateWebSocketOperationContainer(context.extra.socket, operationId);
523
- const graphqlContext = this.buildGraphqlContext(fetchRequest, {
524
- connectionParams: isConnectionParamsRecord(context.connectionParams) ? context.connectionParams : undefined,
525
- request: frameworkRequest,
526
- socket: context.extra.socket
527
- }, operationContainer);
375
+ const fetchRequest = toFetchRequest(request.request);
376
+ const operationContainer = this.getOrCreateWebSocketOperationContainer(request.socket, request.operationId);
528
377
  try {
378
+ const graphqlContext = this.buildGraphqlContext(fetchRequest, {
379
+ connectionParams: request.connectionParams,
380
+ request: request.request,
381
+ socket: request.socket
382
+ }, operationContainer);
529
383
  const {
530
384
  contextFactory,
531
385
  parse,
@@ -535,21 +389,21 @@ class GraphqlLifecycleService {
535
389
  request: fetchRequest,
536
390
  [GRAPHQL_CONTEXT_OVERRIDE]: graphqlContext
537
391
  });
538
- const document = parse(payload.query);
392
+ const document = parse(request.payload.query);
539
393
  const validationErrors = validate(schema, document);
540
394
  if (validationErrors.length > 0) {
541
- await this.disposeWebSocketOperationContainer(context.extra.socket, operationId);
395
+ await this.disposeWebSocketOperationContainer(request.socket, request.operationId);
542
396
  return validationErrors;
543
397
  }
544
398
  return {
545
399
  contextValue: await contextFactory(),
546
400
  document,
547
- operationName: payload.operationName ?? undefined,
401
+ operationName: request.payload.operationName ?? undefined,
548
402
  schema,
549
- variableValues: payload.variables
403
+ variableValues: request.payload.variables
550
404
  };
551
405
  } catch (error) {
552
- await this.disposeWebSocketOperationContainer(context.extra.socket, operationId);
406
+ await this.disposeWebSocketOperationContainer(request.socket, request.operationId);
553
407
  throw error;
554
408
  }
555
409
  }
@@ -568,15 +422,6 @@ class GraphqlLifecycleService {
568
422
  }
569
423
  return new GraphQLError(`GraphQL websocket active operation count exceeds the configured limit of ${String(limits.maxOperationsPerConnection)}.`);
570
424
  }
571
- rejectWebSocketUpgrade(socket, statusCode, message) {
572
- if (!socket.writable) {
573
- socket.destroy();
574
- return;
575
- }
576
- const body = `${message}\n`;
577
- socket.write([`HTTP/1.1 ${String(statusCode)} ${statusCode === 503 ? 'Service Unavailable' : 'Bad Request'}`, 'Connection: close', 'Content-Type: text/plain; charset=utf-8', `Content-Length: ${String(new TextEncoder().encode(body).byteLength)}`, '', body].join('\r\n'));
578
- socket.destroy();
579
- }
580
425
  getOrCreateWebSocketOperationContainer(socketKey, operationId) {
581
426
  const existingSocketContainers = this.websocketOperationContainers.get(socketKey);
582
427
  if (existingSocketContainers?.has(operationId)) {
@@ -598,25 +443,55 @@ class GraphqlLifecycleService {
598
443
  if (socketContainers && socketContainers.size === 0) {
599
444
  this.websocketOperationContainers.delete(socketKey);
600
445
  }
601
- try {
602
- await operationContainer.dispose();
603
- } catch (error) {
604
- this.logger.error('Failed to dispose GraphQL websocket operation container.', error, 'GraphqlLifecycleService');
446
+ return await this.disposeWebSocketContainerOnce(socketKey, operationContainer);
447
+ }
448
+ async disposeWebSocketContainerOnce(socketKey, operationContainer) {
449
+ const inFlight = this.websocketContainerDisposals.get(operationContainer);
450
+ if (inFlight) {
451
+ return await inFlight;
605
452
  }
453
+ const attempted = this.websocketContainersAttemptedInCurrentCleanup;
454
+ if (attempted?.has(operationContainer)) {
455
+ return undefined;
456
+ }
457
+ attempted?.add(operationContainer);
458
+ const disposal = operationContainer.dispose().then(() => {
459
+ this.failedWebsocketOperationContainers.get(socketKey)?.delete(operationContainer);
460
+ return undefined;
461
+ }, error => {
462
+ const failedContainers = this.failedWebsocketOperationContainers.get(socketKey) ?? new Set();
463
+ failedContainers.add(operationContainer);
464
+ this.failedWebsocketOperationContainers.set(socketKey, failedContainers);
465
+ this.logger.error('Failed to dispose GraphQL websocket operation container.', error, 'GraphqlLifecycleService');
466
+ return error;
467
+ }).finally(() => {
468
+ this.websocketContainerDisposals.delete(operationContainer);
469
+ });
470
+ this.websocketContainerDisposals.set(operationContainer, disposal);
471
+ return await disposal;
606
472
  }
607
473
  async disposeAllWebSocketOperationContainers(socketKey) {
608
- const socketContainers = this.websocketOperationContainers.get(socketKey);
609
- if (!socketContainers) {
610
- return;
474
+ const cleanupErrors = [];
475
+ const failedContainers = this.failedWebsocketOperationContainers.get(socketKey);
476
+ for (const operationContainer of [...(failedContainers ?? [])]) {
477
+ const cleanupError = await this.disposeWebSocketContainerOnce(socketKey, operationContainer);
478
+ if (cleanupError !== undefined) {
479
+ collectCleanupError(cleanupErrors, cleanupError);
480
+ }
611
481
  }
612
- this.websocketOperationContainers.delete(socketKey);
613
- for (const operationContainer of socketContainers.values()) {
614
- try {
615
- await operationContainer.dispose();
616
- } catch (error) {
617
- this.logger.error('Failed to dispose GraphQL websocket operation container.', error, 'GraphqlLifecycleService');
482
+ if (failedContainers?.size === 0) {
483
+ this.failedWebsocketOperationContainers.delete(socketKey);
484
+ }
485
+ const socketContainers = this.websocketOperationContainers.get(socketKey);
486
+ for (const operationId of [...(socketContainers?.keys() ?? [])]) {
487
+ const cleanupError = await this.disposeWebSocketOperationContainer(socketKey, operationId);
488
+ if (cleanupError !== undefined) {
489
+ collectCleanupError(cleanupErrors, cleanupError);
618
490
  }
619
491
  }
492
+ if (cleanupErrors.length > 0) {
493
+ throw new AggregateError(cleanupErrors, 'Failed to dispose GraphQL websocket operation containers.');
494
+ }
620
495
  }
621
496
  getOrCreateOperationContainer(request) {
622
497
  const existing = this.operationContainers.get(request);
@@ -632,11 +507,24 @@ class GraphqlLifecycleService {
632
507
  if (!operationContainer) {
633
508
  return;
634
509
  }
635
- this.operationContainers.delete(request);
636
510
  try {
637
511
  await operationContainer.dispose();
638
512
  } catch (error) {
639
513
  this.logger.error('Failed to dispose GraphQL operation container.', error, 'GraphqlLifecycleService');
514
+ return error;
515
+ }
516
+ this.operationContainers.delete(request);
517
+ }
518
+ async disposeAllOperationContainers() {
519
+ const cleanupErrors = [];
520
+ for (const request of [...this.operationContainers.keys()]) {
521
+ const cleanupError = await this.disposeOperationContainer(request);
522
+ if (cleanupError !== undefined) {
523
+ collectCleanupError(cleanupErrors, cleanupError);
524
+ }
525
+ }
526
+ if (cleanupErrors.length > 0) {
527
+ throw new AggregateError(cleanupErrors, 'Failed to dispose GraphQL operation containers.');
640
528
  }
641
529
  }
642
530
  static {