@makerx/graphql-core 3.1.0 → 3.3.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.
- package/README.md +54 -2
- package/deprecation.d.cts +81 -0
- package/deprecation.d.mts +81 -0
- package/deprecation.d.ts +81 -0
- package/deprecation.js +240 -0
- package/deprecation.js.map +1 -0
- package/deprecation.mjs +235 -0
- package/deprecation.mjs.map +1 -0
- package/index.d.cts +1 -0
- package/index.d.mts +1 -0
- package/index.d.ts +1 -0
- package/index.js +5 -0
- package/index.js.map +1 -1
- package/index.mjs +1 -0
- package/index.mjs.map +1 -1
- package/logging.d.cts +17 -2
- package/logging.d.mts +17 -2
- package/logging.d.ts +17 -2
- package/logging.js +21 -3
- package/logging.js.map +1 -1
- package/logging.mjs +21 -3
- package/logging.mjs.map +1 -1
- package/package.json +4 -1
- package/subscriptions/server.d.cts +6 -1
- package/subscriptions/server.d.mts +6 -1
- package/subscriptions/server.d.ts +6 -1
- package/subscriptions/server.js +8 -2
- package/subscriptions/server.js.map +1 -1
- package/subscriptions/server.mjs +8 -2
- package/subscriptions/server.mjs.map +1 -1
- package/subscriptions/subscription-iterator.d.cts +35 -4
- package/subscriptions/subscription-iterator.d.mts +35 -4
- package/subscriptions/subscription-iterator.d.ts +35 -4
- package/subscriptions/subscription-iterator.js +55 -7
- package/subscriptions/subscription-iterator.js.map +1 -1
- package/subscriptions/subscription-iterator.mjs +55 -7
- package/subscriptions/subscription-iterator.mjs.map +1 -1
package/logging.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logging.mjs","sources":["../src/logging.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { isLocalDev } from '@makerx/node-common'\nimport { omitBy } from 'es-toolkit/compat'\nimport type { ExecutionArgs, GraphQLFormattedError } from 'graphql'\nimport { OperationTypeNode, print } from 'graphql'\nimport type { ExecutionResult } from 'graphql-ws'\nimport type { GraphQLContext } from './context'\nimport { isIntrospectionQuery } from './operation'\nimport { isNil } from './utils'\n\ntype LogFunction = Logger['info']\nexport type LoggerLogFunctions<T extends Logger> = {\n [Property in keyof T]: LogFunction\n}\n\n/**\n * Info for logging a GraphQL operation in a consistent format with the option of including any additional data.\n */\nexport interface GraphQLLogOperationInfo<TLogger extends Logger = Logger> extends Record<string, unknown> {\n /**\n * The message to log, defaults to 'GraphQL operation'.\n */\n message?: string\n /**\n * The timestamp when the operation started, if supplied, the duration will be logged.\n */\n started?: number\n /**\n * The type of GraphQL operation.\n */\n type?: OperationTypeNode | null\n /**\n * The name of the GraphQL operation.\n */\n operationName?: string | null\n /**\n * The formatted GraphQL query.\n */\n query?: string | null\n /**\n * The GraphQL variables.\n */\n variables?: Record<string, unknown> | null\n /**\n * The result of the GraphQL operation.\n * Generally, we don't log the data or extensions, just errors.\n */\n result?: {\n data?: Record<string, unknown> | null\n errors?: readonly GraphQLFormattedError[] | null\n extensions?: Record<string, unknown> | null\n hasNext?: boolean\n }\n /**\n * Whether the operation is an introspection query.\n */\n isIntrospectionQuery?: boolean\n /**\n * Whether the operation is part of an incremental response.\n */\n isIncrementalResponse?: boolean\n /**\n * Whether the operation is a subsequent payload of an incremental response.\n */\n isSubsequentPayload?: boolean\n /**\n * The logger to use.\n */\n logger: TLogger\n /**\n * The logger function to use, defaults to 'info'.\n */\n logLevel?: keyof LoggerLogFunctions<TLogger>\n}\n\n/**\n * Logs a GraphQL operation in a consistent format with the option of including any additional data.\n * Top level and result entries with null or undefined values will be omitted.\n */\nexport const logGraphQLOperation = <TLogger extends Logger = Logger>({\n message = 'GraphQL operation',\n started,\n type,\n operationName,\n query,\n variables,\n result,\n logger,\n logLevel = 'info',\n ...rest\n}: GraphQLLogOperationInfo<TLogger>) => {\n const isIntrospection = query && isIntrospectionQuery(query)\n if (isLocalDev && isIntrospection) return\n logger[logLevel as keyof Logger](\n message,\n omitBy(\n {\n type,\n operationName,\n query,\n variables: variables && Object.keys(variables).length > 0 ? variables : undefined,\n duration: started ? Date.now() - started : undefined,\n result: result ? omitBy(result, isNil) : undefined,\n isIntrospectionQuery: isIntrospection || undefined,\n ...omitBy(rest, isNil),\n },\n isNil,\n ),\n )\n}\n\nexport const logSubscriptionOperation = <TLogger extends Logger = Logger>({\n id,\n args,\n result,\n message,\n logLevel,\n resolveLogger,\n}: {\n id?: string\n args: ExecutionArgs\n result?: ExecutionResult\n message?: string\n logLevel?: keyof LoggerLogFunctions<TLogger>\n resolveLogger?: (context: GraphQLContext) => TLogger\n}) => {\n const logger = resolveLogger\n ? resolveLogger(args.contextValue as GraphQLContext)\n : ((args.contextValue as GraphQLContext).logger as TLogger)\n if (!logger) return\n\n const { operationName, variableValues, document } = args\n const { data, ...resultWithoutData } = result ?? {}\n\n logGraphQLOperation({\n message,\n type: OperationTypeNode.SUBSCRIPTION,\n subscriptionId: id,\n operationName,\n query: print(document),\n variables: variableValues,\n result: resultWithoutData,\n logger,\n logLevel,\n })\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"logging.mjs","sources":["../src/logging.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { isLocalDev } from '@makerx/node-common'\nimport { omitBy } from 'es-toolkit/compat'\nimport type { ExecutionArgs, GraphQLFormattedError } from 'graphql'\nimport { OperationTypeNode, print } from 'graphql'\nimport type { ExecutionResult } from 'graphql-ws'\nimport type { GraphQLContext } from './context'\nimport type { DeprecatedElementUsage } from './deprecation'\nimport { collectDeprecatedElementUsage } from './deprecation'\nimport { isIntrospectionQuery } from './operation'\nimport { isNil } from './utils'\n\ntype LogFunction = Logger['info']\nexport type LoggerLogFunctions<T extends Logger> = {\n [Property in keyof T]: LogFunction\n}\n\n/**\n * Info for logging a GraphQL operation in a consistent format with the option of including any additional data.\n */\nexport interface GraphQLLogOperationInfo<TLogger extends Logger = Logger> extends Record<string, unknown> {\n /**\n * The message to log, defaults to 'GraphQL operation'.\n */\n message?: string\n /**\n * The timestamp when the operation started, if supplied, the duration will be logged.\n */\n started?: number\n /**\n * The type of GraphQL operation.\n */\n type?: OperationTypeNode | null\n /**\n * The name of the GraphQL operation.\n */\n operationName?: string | null\n /**\n * The formatted GraphQL query.\n */\n query?: string | null\n /**\n * The GraphQL variables.\n */\n variables?: Record<string, unknown> | null\n /**\n * The result of the GraphQL operation.\n * Generally, we don't log the data or extensions, just errors.\n */\n result?: {\n data?: Record<string, unknown> | null\n errors?: readonly GraphQLFormattedError[] | null\n extensions?: Record<string, unknown> | null\n hasNext?: boolean\n }\n /**\n * Whether the operation is an introspection query.\n */\n isIntrospectionQuery?: boolean\n /**\n * Whether the operation is part of an incremental response.\n */\n isIncrementalResponse?: boolean\n /**\n * Whether the operation is a subsequent payload of an incremental response.\n */\n isSubsequentPayload?: boolean\n /**\n * Deprecated schema elements the operation used, as returned by `collectDeprecatedElementUsage`.\n * Omitted from the log entry when empty.\n */\n deprecatedElements?: DeprecatedElementUsage[]\n /**\n * Whether a limit stopped deprecated element collection, meaning `deprecatedElements` may be\n * incomplete. Omitted from the log entry when false.\n */\n deprecatedElementsTruncated?: boolean\n /**\n * The logger to use.\n */\n logger: TLogger\n /**\n * The logger function to use, defaults to 'info'.\n */\n logLevel?: keyof LoggerLogFunctions<TLogger>\n}\n\n/**\n * Logs a GraphQL operation in a consistent format with the option of including any additional data.\n * Top level and result entries with null or undefined values will be omitted.\n */\nexport const logGraphQLOperation = <TLogger extends Logger = Logger>({\n message = 'GraphQL operation',\n started,\n type,\n operationName,\n query,\n variables,\n result,\n deprecatedElements,\n deprecatedElementsTruncated,\n logger,\n logLevel = 'info',\n ...rest\n}: GraphQLLogOperationInfo<TLogger>) => {\n const isIntrospection = query && isIntrospectionQuery(query)\n if (isLocalDev && isIntrospection) return\n logger[logLevel as keyof Logger](\n message,\n omitBy(\n {\n type,\n operationName,\n query,\n variables: variables && Object.keys(variables).length > 0 ? variables : undefined,\n duration: started ? Date.now() - started : undefined,\n result: result ? omitBy(result, isNil) : undefined,\n isIntrospectionQuery: isIntrospection || undefined,\n deprecatedElements: deprecatedElements?.length ? deprecatedElements : undefined,\n deprecatedElementsTruncated: deprecatedElementsTruncated || undefined,\n ...omitBy(rest, isNil),\n },\n isNil,\n ),\n )\n}\n\nexport const logSubscriptionOperation = <TLogger extends Logger = Logger>({\n id,\n args,\n result,\n message,\n logLevel,\n resolveLogger,\n includeDeprecatedElements,\n}: {\n id?: string\n args: ExecutionArgs\n result?: ExecutionResult\n message?: string\n logLevel?: keyof LoggerLogFunctions<TLogger>\n resolveLogger?: (context: GraphQLContext) => TLogger\n /**\n * If true, deprecated schema elements the operation uses are collected and logged.\n */\n includeDeprecatedElements?: boolean\n}) => {\n const logger = resolveLogger\n ? resolveLogger(args.contextValue as GraphQLContext)\n : ((args.contextValue as GraphQLContext).logger as TLogger)\n if (!logger) return\n\n const { operationName, variableValues, document, schema } = args\n const { data, ...resultWithoutData } = result ?? {}\n\n let deprecatedElements: DeprecatedElementUsage[] | undefined\n let deprecatedElementsTruncated: boolean | undefined\n if (includeDeprecatedElements) {\n try {\n const usage = collectDeprecatedElementUsage({ schema, document, operationName, variables: variableValues })\n deprecatedElements = usage.elements\n deprecatedElementsTruncated = usage.truncated\n } catch (error) {\n // Telemetry must never take down the subscription that produced it.\n logger.warn('Failed to collect deprecated schema element usage', { error, operationName })\n }\n }\n\n logGraphQLOperation({\n message,\n type: OperationTypeNode.SUBSCRIPTION,\n subscriptionId: id,\n operationName,\n query: print(document),\n variables: variableValues,\n result: resultWithoutData,\n deprecatedElements,\n deprecatedElementsTruncated,\n logger,\n logLevel,\n })\n}\n"],"names":[],"mappings":";;;;;;;AAuFA;;;AAGG;AACI,MAAM,mBAAmB,GAAG,CAAkC,EACnE,OAAO,GAAG,mBAAmB,EAC7B,OAAO,EACP,IAAI,EACJ,aAAa,EACb,KAAK,EACL,SAAS,EACT,MAAM,EACN,kBAAkB,EAClB,2BAA2B,EAC3B,MAAM,EACN,QAAQ,GAAG,MAAM,EACjB,GAAG,IAAI,EAC0B,KAAI;IACrC,MAAM,eAAe,GAAG,KAAK,IAAI,oBAAoB,CAAC,KAAK,CAAC;IAC5D,IAAI,UAAU,IAAI,eAAe;QAAE;AACnC,IAAA,MAAM,CAAC,QAAwB,CAAC,CAC9B,OAAO,EACP,MAAM,CACJ;QACE,IAAI;QACJ,aAAa;QACb,KAAK;QACL,SAAS,EAAE,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,SAAS,GAAG,SAAS;AACjF,QAAA,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,SAAS;AACpD,QAAA,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,SAAS;QAClD,oBAAoB,EAAE,eAAe,IAAI,SAAS;QAClD,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;QAC/E,2BAA2B,EAAE,2BAA2B,IAAI,SAAS;AACrE,QAAA,GAAG,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC;KACvB,EACD,KAAK,CACN,CACF;AACH;MAEa,wBAAwB,GAAG,CAAkC,EACxE,EAAE,EACF,IAAI,EACJ,MAAM,EACN,OAAO,EACP,QAAQ,EACR,aAAa,EACb,yBAAyB,GAY1B,KAAI;IACH,MAAM,MAAM,GAAG;AACb,UAAE,aAAa,CAAC,IAAI,CAAC,YAA8B;AACnD,UAAI,IAAI,CAAC,YAA+B,CAAC,MAAkB;AAC7D,IAAA,IAAI,CAAC,MAAM;QAAE;IAEb,MAAM,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI;IAChE,MAAM,EAAE,IAAI,EAAE,GAAG,iBAAiB,EAAE,GAAG,MAAM,IAAI,EAAE;AAEnD,IAAA,IAAI,kBAAwD;AAC5D,IAAA,IAAI,2BAAgD;IACpD,IAAI,yBAAyB,EAAE;AAC7B,QAAA,IAAI;AACF,YAAA,MAAM,KAAK,GAAG,6BAA6B,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC;AAC3G,YAAA,kBAAkB,GAAG,KAAK,CAAC,QAAQ;AACnC,YAAA,2BAA2B,GAAG,KAAK,CAAC,SAAS;QAC/C;QAAE,OAAO,KAAK,EAAE;;YAEd,MAAM,CAAC,IAAI,CAAC,mDAAmD,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;QAC5F;IACF;AAEA,IAAA,mBAAmB,CAAC;QAClB,OAAO;QACP,IAAI,EAAE,iBAAiB,CAAC,YAAY;AACpC,QAAA,cAAc,EAAE,EAAE;QAClB,aAAa;AACb,QAAA,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC;AACtB,QAAA,SAAS,EAAE,cAAc;AACzB,QAAA,MAAM,EAAE,iBAAiB;QACzB,kBAAkB;QAClB,2BAA2B;QAC3B,MAAM;QACN,QAAQ;AACT,KAAA,CAAC;AACJ;;;;"}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"**"
|
|
5
5
|
],
|
|
6
6
|
"name": "@makerx/graphql-core",
|
|
7
|
-
"version": "3.
|
|
7
|
+
"version": "3.3.0",
|
|
8
8
|
"private": false,
|
|
9
9
|
"description": "A set of core GraphQL utilities that MakerX uses to build GraphQL APIs",
|
|
10
10
|
"author": "MakerX",
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
"optional": true
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
|
+
"overrides": {
|
|
45
|
+
"esbuild": "^0.28.1"
|
|
46
|
+
},
|
|
44
47
|
"main": "./index.js",
|
|
45
48
|
"module": "./index.mjs",
|
|
46
49
|
"types": "./index.d.ts",
|
|
@@ -3,7 +3,7 @@ import type { GraphQLSchema } from 'graphql';
|
|
|
3
3
|
import type { Server } from 'http';
|
|
4
4
|
import type { GraphQLContext, JwtPayload } from '../context';
|
|
5
5
|
import type { CreateSubscriptionContext } from './context';
|
|
6
|
-
export declare function useSubscriptionsServer<TLogger extends Logger = Logger>({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel, path, verifyToken, requireAuth, jwtClaimsToLog, resolveSubscriptionOperationLogger, }: {
|
|
6
|
+
export declare function useSubscriptionsServer<TLogger extends Logger = Logger>({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel, path, verifyToken, requireAuth, jwtClaimsToLog, resolveSubscriptionOperationLogger, includeDeprecatedElements, }: {
|
|
7
7
|
schema: GraphQLSchema;
|
|
8
8
|
httpServer: Server;
|
|
9
9
|
createSubscriptionContext: CreateSubscriptionContext;
|
|
@@ -14,4 +14,9 @@ export declare function useSubscriptionsServer<TLogger extends Logger = Logger>(
|
|
|
14
14
|
requireAuth?: boolean;
|
|
15
15
|
jwtClaimsToLog?: string[];
|
|
16
16
|
resolveSubscriptionOperationLogger?: (context: GraphQLContext) => TLogger;
|
|
17
|
+
/**
|
|
18
|
+
* If true, deprecated schema elements a subscription uses are collected and logged when it is
|
|
19
|
+
* established. Not collected per emitted payload: the usage is a property of the operation.
|
|
20
|
+
*/
|
|
21
|
+
includeDeprecatedElements?: boolean;
|
|
17
22
|
}): import("graphql-ws").Disposable;
|
|
@@ -3,7 +3,7 @@ import type { GraphQLSchema } from 'graphql';
|
|
|
3
3
|
import type { Server } from 'http';
|
|
4
4
|
import type { GraphQLContext, JwtPayload } from '../context.mjs';
|
|
5
5
|
import type { CreateSubscriptionContext } from './context.mjs';
|
|
6
|
-
export declare function useSubscriptionsServer<TLogger extends Logger = Logger>({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel, path, verifyToken, requireAuth, jwtClaimsToLog, resolveSubscriptionOperationLogger, }: {
|
|
6
|
+
export declare function useSubscriptionsServer<TLogger extends Logger = Logger>({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel, path, verifyToken, requireAuth, jwtClaimsToLog, resolveSubscriptionOperationLogger, includeDeprecatedElements, }: {
|
|
7
7
|
schema: GraphQLSchema;
|
|
8
8
|
httpServer: Server;
|
|
9
9
|
createSubscriptionContext: CreateSubscriptionContext;
|
|
@@ -14,4 +14,9 @@ export declare function useSubscriptionsServer<TLogger extends Logger = Logger>(
|
|
|
14
14
|
requireAuth?: boolean;
|
|
15
15
|
jwtClaimsToLog?: string[];
|
|
16
16
|
resolveSubscriptionOperationLogger?: (context: GraphQLContext) => TLogger;
|
|
17
|
+
/**
|
|
18
|
+
* If true, deprecated schema elements a subscription uses are collected and logged when it is
|
|
19
|
+
* established. Not collected per emitted payload: the usage is a property of the operation.
|
|
20
|
+
*/
|
|
21
|
+
includeDeprecatedElements?: boolean;
|
|
17
22
|
}): import("graphql-ws").Disposable;
|
|
@@ -3,7 +3,7 @@ import type { GraphQLSchema } from 'graphql';
|
|
|
3
3
|
import type { Server } from 'http';
|
|
4
4
|
import type { GraphQLContext, JwtPayload } from '../context';
|
|
5
5
|
import type { CreateSubscriptionContext } from './context';
|
|
6
|
-
export declare function useSubscriptionsServer<TLogger extends Logger = Logger>({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel, path, verifyToken, requireAuth, jwtClaimsToLog, resolveSubscriptionOperationLogger, }: {
|
|
6
|
+
export declare function useSubscriptionsServer<TLogger extends Logger = Logger>({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel, path, verifyToken, requireAuth, jwtClaimsToLog, resolveSubscriptionOperationLogger, includeDeprecatedElements, }: {
|
|
7
7
|
schema: GraphQLSchema;
|
|
8
8
|
httpServer: Server;
|
|
9
9
|
createSubscriptionContext: CreateSubscriptionContext;
|
|
@@ -14,4 +14,9 @@ export declare function useSubscriptionsServer<TLogger extends Logger = Logger>(
|
|
|
14
14
|
requireAuth?: boolean;
|
|
15
15
|
jwtClaimsToLog?: string[];
|
|
16
16
|
resolveSubscriptionOperationLogger?: (context: GraphQLContext) => TLogger;
|
|
17
|
+
/**
|
|
18
|
+
* If true, deprecated schema elements a subscription uses are collected and logged when it is
|
|
19
|
+
* established. Not collected per emitted payload: the usage is a property of the operation.
|
|
20
|
+
*/
|
|
21
|
+
includeDeprecatedElements?: boolean;
|
|
17
22
|
}): import("graphql-ws").Disposable;
|
package/subscriptions/server.js
CHANGED
|
@@ -7,7 +7,7 @@ var ws = require('ws');
|
|
|
7
7
|
var logging = require('../logging.js');
|
|
8
8
|
var utils = require('./utils.js');
|
|
9
9
|
|
|
10
|
-
function useSubscriptionsServer({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel = 'info', path = '/graphql', verifyToken, requireAuth, jwtClaimsToLog = ['oid', 'iss'], resolveSubscriptionOperationLogger, }) {
|
|
10
|
+
function useSubscriptionsServer({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel = 'info', path = '/graphql', verifyToken, requireAuth, jwtClaimsToLog = ['oid', 'iss'], resolveSubscriptionOperationLogger, includeDeprecatedElements, }) {
|
|
11
11
|
if (requireAuth && !verifyToken)
|
|
12
12
|
throw new Error('verifyToken must be supplied when requireAuth is true');
|
|
13
13
|
const wsServer = new ws.WebSocketServer({
|
|
@@ -77,7 +77,13 @@ function useSubscriptionsServer({ schema, httpServer, createSubscriptionContext,
|
|
|
77
77
|
});
|
|
78
78
|
},
|
|
79
79
|
onOperation(_ctx, id, _payload, args) {
|
|
80
|
-
logging.logSubscriptionOperation({
|
|
80
|
+
logging.logSubscriptionOperation({
|
|
81
|
+
id,
|
|
82
|
+
args,
|
|
83
|
+
logLevel: operationLogLevel,
|
|
84
|
+
resolveLogger: resolveSubscriptionOperationLogger,
|
|
85
|
+
includeDeprecatedElements,
|
|
86
|
+
});
|
|
81
87
|
},
|
|
82
88
|
onNext(_ctx, id, _payload, args, { data, ...result }) {
|
|
83
89
|
logging.logSubscriptionOperation({ id, args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sources":["../../src/subscriptions/server.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { pick } from 'es-toolkit/compat'\nimport type { GraphQLSchema } from 'graphql'\nimport { CloseCode } from 'graphql-ws'\nimport { useServer } from 'graphql-ws/use/ws'\nimport type { Server } from 'http'\nimport { WebSocketServer } from 'ws'\nimport type { GraphQLContext, JwtPayload } from '../context'\nimport { logSubscriptionOperation } from '../logging'\nimport type { CreateSubscriptionContext } from './context'\nimport { extractTokenFromConnectionParams, getHost } from './utils'\n\nexport function useSubscriptionsServer<TLogger extends Logger = Logger>({\n schema,\n httpServer,\n createSubscriptionContext,\n logger,\n operationLogLevel = 'info',\n path = '/graphql',\n verifyToken,\n requireAuth,\n jwtClaimsToLog = ['oid', 'iss'],\n resolveSubscriptionOperationLogger,\n}: {\n schema: GraphQLSchema\n httpServer: Server\n createSubscriptionContext: CreateSubscriptionContext\n logger: TLogger\n operationLogLevel?: keyof TLogger\n path?: string\n verifyToken?: (host: string, token: string) => Promise<JwtPayload>\n requireAuth?: boolean\n jwtClaimsToLog?: string[]\n resolveSubscriptionOperationLogger?: (context: GraphQLContext) => TLogger\n}) {\n if (requireAuth && !verifyToken) throw new Error('verifyToken must be supplied when requireAuth is true')\n\n const wsServer = new WebSocketServer({\n server: httpServer,\n path,\n })\n\n return useServer(\n {\n schema,\n onError(_ctx, message, errors) {\n logger.error('GraphQL subscriptions server error', { message, errors })\n },\n onConnect: async (ctx) => {\n const connectionEstablished = 'Subscription connection established'\n if (!verifyToken) {\n logger.info(connectionEstablished)\n return true\n }\n\n const token = extractTokenFromConnectionParams(ctx.connectionParams)\n if (!token) {\n if (requireAuth) {\n logger.error('No authorization parameter was supplied via websocket connection params')\n return false\n }\n logger.info(connectionEstablished)\n return true\n }\n\n try {\n const claims = await verifyToken(getHost(ctx.extra.request), token)\n ctx.extra.claims = claims as unknown as undefined\n logger.info(connectionEstablished, {\n claims: pick(claims, jwtClaimsToLog),\n })\n return true\n } catch (error) {\n logger.error('Failed to verify subscription connection auth token', { error })\n return false\n }\n },\n onSubscribe: async (ctx) => {\n if (!verifyToken) return\n const token = extractTokenFromConnectionParams(ctx.connectionParams)\n if (!token) {\n if (requireAuth) {\n logger.error('No authorization parameter was supplied via websocket connection params')\n ctx.extra.socket.close(CloseCode.Forbidden, 'Forbidden')\n }\n return\n }\n try {\n await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.warn('Subscription connection auth token is no longer valid', { claims: pick(ctx.extra.claims, jwtClaimsToLog), error })\n ctx.extra.socket.close(CloseCode.Forbidden, 'Forbidden')\n }\n },\n onDisconnect({ extra: { claims } }) {\n logger.info('Subscription connection disconnected', { claims: pick(claims, jwtClaimsToLog) })\n },\n context: async (ctx) => {\n return createSubscriptionContext({\n connectRequest: ctx.extra.request,\n connectionParams: ctx.connectionParams,\n claims: ctx.extra.claims as JwtPayload | undefined,\n })\n },\n onOperation(_ctx, id, _payload, args) {\n logSubscriptionOperation({
|
|
1
|
+
{"version":3,"file":"server.js","sources":["../../src/subscriptions/server.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { pick } from 'es-toolkit/compat'\nimport type { GraphQLSchema } from 'graphql'\nimport { CloseCode } from 'graphql-ws'\nimport { useServer } from 'graphql-ws/use/ws'\nimport type { Server } from 'http'\nimport { WebSocketServer } from 'ws'\nimport type { GraphQLContext, JwtPayload } from '../context'\nimport { logSubscriptionOperation } from '../logging'\nimport type { CreateSubscriptionContext } from './context'\nimport { extractTokenFromConnectionParams, getHost } from './utils'\n\nexport function useSubscriptionsServer<TLogger extends Logger = Logger>({\n schema,\n httpServer,\n createSubscriptionContext,\n logger,\n operationLogLevel = 'info',\n path = '/graphql',\n verifyToken,\n requireAuth,\n jwtClaimsToLog = ['oid', 'iss'],\n resolveSubscriptionOperationLogger,\n includeDeprecatedElements,\n}: {\n schema: GraphQLSchema\n httpServer: Server\n createSubscriptionContext: CreateSubscriptionContext\n logger: TLogger\n operationLogLevel?: keyof TLogger\n path?: string\n verifyToken?: (host: string, token: string) => Promise<JwtPayload>\n requireAuth?: boolean\n jwtClaimsToLog?: string[]\n resolveSubscriptionOperationLogger?: (context: GraphQLContext) => TLogger\n /**\n * If true, deprecated schema elements a subscription uses are collected and logged when it is\n * established. Not collected per emitted payload: the usage is a property of the operation.\n */\n includeDeprecatedElements?: boolean\n}) {\n if (requireAuth && !verifyToken) throw new Error('verifyToken must be supplied when requireAuth is true')\n\n const wsServer = new WebSocketServer({\n server: httpServer,\n path,\n })\n\n return useServer(\n {\n schema,\n onError(_ctx, message, errors) {\n logger.error('GraphQL subscriptions server error', { message, errors })\n },\n onConnect: async (ctx) => {\n const connectionEstablished = 'Subscription connection established'\n if (!verifyToken) {\n logger.info(connectionEstablished)\n return true\n }\n\n const token = extractTokenFromConnectionParams(ctx.connectionParams)\n if (!token) {\n if (requireAuth) {\n logger.error('No authorization parameter was supplied via websocket connection params')\n return false\n }\n logger.info(connectionEstablished)\n return true\n }\n\n try {\n const claims = await verifyToken(getHost(ctx.extra.request), token)\n ctx.extra.claims = claims as unknown as undefined\n logger.info(connectionEstablished, {\n claims: pick(claims, jwtClaimsToLog),\n })\n return true\n } catch (error) {\n logger.error('Failed to verify subscription connection auth token', { error })\n return false\n }\n },\n onSubscribe: async (ctx) => {\n if (!verifyToken) return\n const token = extractTokenFromConnectionParams(ctx.connectionParams)\n if (!token) {\n if (requireAuth) {\n logger.error('No authorization parameter was supplied via websocket connection params')\n ctx.extra.socket.close(CloseCode.Forbidden, 'Forbidden')\n }\n return\n }\n try {\n await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.warn('Subscription connection auth token is no longer valid', { claims: pick(ctx.extra.claims, jwtClaimsToLog), error })\n ctx.extra.socket.close(CloseCode.Forbidden, 'Forbidden')\n }\n },\n onDisconnect({ extra: { claims } }) {\n logger.info('Subscription connection disconnected', { claims: pick(claims, jwtClaimsToLog) })\n },\n context: async (ctx) => {\n return createSubscriptionContext({\n connectRequest: ctx.extra.request,\n connectionParams: ctx.connectionParams,\n claims: ctx.extra.claims as JwtPayload | undefined,\n })\n },\n onOperation(_ctx, id, _payload, args) {\n logSubscriptionOperation({\n id,\n args,\n logLevel: operationLogLevel,\n resolveLogger: resolveSubscriptionOperationLogger,\n includeDeprecatedElements,\n })\n },\n onNext(_ctx, id, _payload, args, { data, ...result }) {\n logSubscriptionOperation({ id, args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger })\n },\n },\n wsServer,\n )\n}\n"],"names":["WebSocketServer","useServer","extractTokenFromConnectionParams","getHost","pick","CloseCode","logSubscriptionOperation"],"mappings":"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"}
|
package/subscriptions/server.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { WebSocketServer } from 'ws';
|
|
|
5
5
|
import { logSubscriptionOperation } from '../logging.mjs';
|
|
6
6
|
import { extractTokenFromConnectionParams, getHost } from './utils.mjs';
|
|
7
7
|
|
|
8
|
-
function useSubscriptionsServer({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel = 'info', path = '/graphql', verifyToken, requireAuth, jwtClaimsToLog = ['oid', 'iss'], resolveSubscriptionOperationLogger, }) {
|
|
8
|
+
function useSubscriptionsServer({ schema, httpServer, createSubscriptionContext, logger, operationLogLevel = 'info', path = '/graphql', verifyToken, requireAuth, jwtClaimsToLog = ['oid', 'iss'], resolveSubscriptionOperationLogger, includeDeprecatedElements, }) {
|
|
9
9
|
if (requireAuth && !verifyToken)
|
|
10
10
|
throw new Error('verifyToken must be supplied when requireAuth is true');
|
|
11
11
|
const wsServer = new WebSocketServer({
|
|
@@ -75,7 +75,13 @@ function useSubscriptionsServer({ schema, httpServer, createSubscriptionContext,
|
|
|
75
75
|
});
|
|
76
76
|
},
|
|
77
77
|
onOperation(_ctx, id, _payload, args) {
|
|
78
|
-
logSubscriptionOperation({
|
|
78
|
+
logSubscriptionOperation({
|
|
79
|
+
id,
|
|
80
|
+
args,
|
|
81
|
+
logLevel: operationLogLevel,
|
|
82
|
+
resolveLogger: resolveSubscriptionOperationLogger,
|
|
83
|
+
includeDeprecatedElements,
|
|
84
|
+
});
|
|
79
85
|
},
|
|
80
86
|
onNext(_ctx, id, _payload, args, { data, ...result }) {
|
|
81
87
|
logSubscriptionOperation({ id, args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","sources":["../../src/subscriptions/server.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { pick } from 'es-toolkit/compat'\nimport type { GraphQLSchema } from 'graphql'\nimport { CloseCode } from 'graphql-ws'\nimport { useServer } from 'graphql-ws/use/ws'\nimport type { Server } from 'http'\nimport { WebSocketServer } from 'ws'\nimport type { GraphQLContext, JwtPayload } from '../context'\nimport { logSubscriptionOperation } from '../logging'\nimport type { CreateSubscriptionContext } from './context'\nimport { extractTokenFromConnectionParams, getHost } from './utils'\n\nexport function useSubscriptionsServer<TLogger extends Logger = Logger>({\n schema,\n httpServer,\n createSubscriptionContext,\n logger,\n operationLogLevel = 'info',\n path = '/graphql',\n verifyToken,\n requireAuth,\n jwtClaimsToLog = ['oid', 'iss'],\n resolveSubscriptionOperationLogger,\n}: {\n schema: GraphQLSchema\n httpServer: Server\n createSubscriptionContext: CreateSubscriptionContext\n logger: TLogger\n operationLogLevel?: keyof TLogger\n path?: string\n verifyToken?: (host: string, token: string) => Promise<JwtPayload>\n requireAuth?: boolean\n jwtClaimsToLog?: string[]\n resolveSubscriptionOperationLogger?: (context: GraphQLContext) => TLogger\n}) {\n if (requireAuth && !verifyToken) throw new Error('verifyToken must be supplied when requireAuth is true')\n\n const wsServer = new WebSocketServer({\n server: httpServer,\n path,\n })\n\n return useServer(\n {\n schema,\n onError(_ctx, message, errors) {\n logger.error('GraphQL subscriptions server error', { message, errors })\n },\n onConnect: async (ctx) => {\n const connectionEstablished = 'Subscription connection established'\n if (!verifyToken) {\n logger.info(connectionEstablished)\n return true\n }\n\n const token = extractTokenFromConnectionParams(ctx.connectionParams)\n if (!token) {\n if (requireAuth) {\n logger.error('No authorization parameter was supplied via websocket connection params')\n return false\n }\n logger.info(connectionEstablished)\n return true\n }\n\n try {\n const claims = await verifyToken(getHost(ctx.extra.request), token)\n ctx.extra.claims = claims as unknown as undefined\n logger.info(connectionEstablished, {\n claims: pick(claims, jwtClaimsToLog),\n })\n return true\n } catch (error) {\n logger.error('Failed to verify subscription connection auth token', { error })\n return false\n }\n },\n onSubscribe: async (ctx) => {\n if (!verifyToken) return\n const token = extractTokenFromConnectionParams(ctx.connectionParams)\n if (!token) {\n if (requireAuth) {\n logger.error('No authorization parameter was supplied via websocket connection params')\n ctx.extra.socket.close(CloseCode.Forbidden, 'Forbidden')\n }\n return\n }\n try {\n await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.warn('Subscription connection auth token is no longer valid', { claims: pick(ctx.extra.claims, jwtClaimsToLog), error })\n ctx.extra.socket.close(CloseCode.Forbidden, 'Forbidden')\n }\n },\n onDisconnect({ extra: { claims } }) {\n logger.info('Subscription connection disconnected', { claims: pick(claims, jwtClaimsToLog) })\n },\n context: async (ctx) => {\n return createSubscriptionContext({\n connectRequest: ctx.extra.request,\n connectionParams: ctx.connectionParams,\n claims: ctx.extra.claims as JwtPayload | undefined,\n })\n },\n onOperation(_ctx, id, _payload, args) {\n logSubscriptionOperation({
|
|
1
|
+
{"version":3,"file":"server.mjs","sources":["../../src/subscriptions/server.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { pick } from 'es-toolkit/compat'\nimport type { GraphQLSchema } from 'graphql'\nimport { CloseCode } from 'graphql-ws'\nimport { useServer } from 'graphql-ws/use/ws'\nimport type { Server } from 'http'\nimport { WebSocketServer } from 'ws'\nimport type { GraphQLContext, JwtPayload } from '../context'\nimport { logSubscriptionOperation } from '../logging'\nimport type { CreateSubscriptionContext } from './context'\nimport { extractTokenFromConnectionParams, getHost } from './utils'\n\nexport function useSubscriptionsServer<TLogger extends Logger = Logger>({\n schema,\n httpServer,\n createSubscriptionContext,\n logger,\n operationLogLevel = 'info',\n path = '/graphql',\n verifyToken,\n requireAuth,\n jwtClaimsToLog = ['oid', 'iss'],\n resolveSubscriptionOperationLogger,\n includeDeprecatedElements,\n}: {\n schema: GraphQLSchema\n httpServer: Server\n createSubscriptionContext: CreateSubscriptionContext\n logger: TLogger\n operationLogLevel?: keyof TLogger\n path?: string\n verifyToken?: (host: string, token: string) => Promise<JwtPayload>\n requireAuth?: boolean\n jwtClaimsToLog?: string[]\n resolveSubscriptionOperationLogger?: (context: GraphQLContext) => TLogger\n /**\n * If true, deprecated schema elements a subscription uses are collected and logged when it is\n * established. Not collected per emitted payload: the usage is a property of the operation.\n */\n includeDeprecatedElements?: boolean\n}) {\n if (requireAuth && !verifyToken) throw new Error('verifyToken must be supplied when requireAuth is true')\n\n const wsServer = new WebSocketServer({\n server: httpServer,\n path,\n })\n\n return useServer(\n {\n schema,\n onError(_ctx, message, errors) {\n logger.error('GraphQL subscriptions server error', { message, errors })\n },\n onConnect: async (ctx) => {\n const connectionEstablished = 'Subscription connection established'\n if (!verifyToken) {\n logger.info(connectionEstablished)\n return true\n }\n\n const token = extractTokenFromConnectionParams(ctx.connectionParams)\n if (!token) {\n if (requireAuth) {\n logger.error('No authorization parameter was supplied via websocket connection params')\n return false\n }\n logger.info(connectionEstablished)\n return true\n }\n\n try {\n const claims = await verifyToken(getHost(ctx.extra.request), token)\n ctx.extra.claims = claims as unknown as undefined\n logger.info(connectionEstablished, {\n claims: pick(claims, jwtClaimsToLog),\n })\n return true\n } catch (error) {\n logger.error('Failed to verify subscription connection auth token', { error })\n return false\n }\n },\n onSubscribe: async (ctx) => {\n if (!verifyToken) return\n const token = extractTokenFromConnectionParams(ctx.connectionParams)\n if (!token) {\n if (requireAuth) {\n logger.error('No authorization parameter was supplied via websocket connection params')\n ctx.extra.socket.close(CloseCode.Forbidden, 'Forbidden')\n }\n return\n }\n try {\n await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.warn('Subscription connection auth token is no longer valid', { claims: pick(ctx.extra.claims, jwtClaimsToLog), error })\n ctx.extra.socket.close(CloseCode.Forbidden, 'Forbidden')\n }\n },\n onDisconnect({ extra: { claims } }) {\n logger.info('Subscription connection disconnected', { claims: pick(claims, jwtClaimsToLog) })\n },\n context: async (ctx) => {\n return createSubscriptionContext({\n connectRequest: ctx.extra.request,\n connectionParams: ctx.connectionParams,\n claims: ctx.extra.claims as JwtPayload | undefined,\n })\n },\n onOperation(_ctx, id, _payload, args) {\n logSubscriptionOperation({\n id,\n args,\n logLevel: operationLogLevel,\n resolveLogger: resolveSubscriptionOperationLogger,\n includeDeprecatedElements,\n })\n },\n onNext(_ctx, id, _payload, args, { data, ...result }) {\n logSubscriptionOperation({ id, args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger })\n },\n },\n wsServer,\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A function that lazily produces an initial payload. Provided in place of a plain value when the
|
|
3
|
+
* caller needs the wrapped iterator's subscription to be established *before* the payload is read.
|
|
4
|
+
*
|
|
5
|
+
* May return `undefined` (or a promise of it) to indicate "no snapshot" — e.g. a repository
|
|
6
|
+
* `findOne` that yields nothing — in which case iteration proceeds straight to the wrapped stream.
|
|
7
|
+
*/
|
|
8
|
+
export type InitialPayloadFactory<TEventData> = () => TEventData | TEventData[] | undefined | Promise<TEventData | TEventData[] | undefined>;
|
|
9
|
+
/**
|
|
10
|
+
* An initial payload: a single event, an array of events, or a factory that produces either (or
|
|
11
|
+
* `undefined` for "no snapshot").
|
|
12
|
+
*
|
|
13
|
+
* Note: the factory form is detected via `typeof initialPayload === 'function'`. If `TEventData` is
|
|
14
|
+
* itself a callable type, a value-form payload would be misclassified as a factory — wrap such a value
|
|
15
|
+
* in an array (`[value]`) to disambiguate. GraphQL event payloads are objects, so this is not a concern
|
|
16
|
+
* in practice.
|
|
17
|
+
*/
|
|
18
|
+
export type InitialPayload<TEventData> = TEventData | TEventData[] | InitialPayloadFactory<TEventData>;
|
|
1
19
|
/**
|
|
2
20
|
* Wraps an async iterator with options for:
|
|
3
21
|
* - returning an initial payload (e.g. current state)
|
|
@@ -8,16 +26,29 @@
|
|
|
8
26
|
*
|
|
9
27
|
* Eagerly ending iteration when a final event ensures subscriptions do not continue unnecessarily.
|
|
10
28
|
*
|
|
29
|
+
* ### Avoiding the subscribe-before-snapshot race
|
|
30
|
+
*
|
|
31
|
+
* `graphql-subscriptions`' iterator subscribes to its channel lazily, on its first `next()`. If you read the
|
|
32
|
+
* snapshot (the initial payload) *before* constructing/pulling the iterator, any event published between the
|
|
33
|
+
* snapshot read and the subscription going live is lost — and missing from the snapshot too. To close that
|
|
34
|
+
* window, pass `initialPayload` as a **factory** (`() => ... | Promise<...>`). When a factory is supplied this
|
|
35
|
+
* helper eagerly pulls the wrapped iterator once (establishing the subscription) *before* invoking the factory
|
|
36
|
+
* to read the snapshot; events fired during the snapshot read are then buffered by the wrapped iterator instead
|
|
37
|
+
* of dropped, and are delivered immediately after the snapshot.
|
|
38
|
+
*
|
|
11
39
|
* @param iterator The async iterator to wrap.
|
|
12
|
-
* @param initialPayload An optional initial payload to return on the first iteration. May be a single event
|
|
40
|
+
* @param initialPayload An optional initial payload to return on the first iteration. May be a single event, an
|
|
41
|
+
* array, or a factory (`() => event | event[] | Promise<event | event[]>`). Prefer the factory form for the
|
|
42
|
+
* "snapshot then stream" pattern so the subscription is established before the snapshot is read.
|
|
13
43
|
* @param eventIsFinal An optional function to determine if an event is final (and iteration should end).
|
|
14
44
|
* The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which
|
|
15
|
-
* the wrapped iterator is closed
|
|
16
|
-
* is
|
|
45
|
+
* the wrapped iterator is closed (`wrapped.return()`). In the value form it is closed without ever being pulled; in
|
|
46
|
+
* the factory form the single eager pull used to establish the subscription is discarded on teardown. For events
|
|
47
|
+
* from the wrapped iterator, every event is checked.
|
|
17
48
|
* @returns An async iterator for the event data.
|
|
18
49
|
*/
|
|
19
50
|
export declare function wrapSubscriptionIterator<TEventData>({ iterator: wrapped, initialPayload, eventIsFinal, }: {
|
|
20
51
|
iterator: AsyncIterableIterator<TEventData>;
|
|
21
|
-
initialPayload?: TEventData
|
|
52
|
+
initialPayload?: InitialPayload<TEventData>;
|
|
22
53
|
eventIsFinal?: (event: TEventData) => boolean;
|
|
23
54
|
}): AsyncIterator<TEventData> & AsyncIterable<TEventData>;
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A function that lazily produces an initial payload. Provided in place of a plain value when the
|
|
3
|
+
* caller needs the wrapped iterator's subscription to be established *before* the payload is read.
|
|
4
|
+
*
|
|
5
|
+
* May return `undefined` (or a promise of it) to indicate "no snapshot" — e.g. a repository
|
|
6
|
+
* `findOne` that yields nothing — in which case iteration proceeds straight to the wrapped stream.
|
|
7
|
+
*/
|
|
8
|
+
export type InitialPayloadFactory<TEventData> = () => TEventData | TEventData[] | undefined | Promise<TEventData | TEventData[] | undefined>;
|
|
9
|
+
/**
|
|
10
|
+
* An initial payload: a single event, an array of events, or a factory that produces either (or
|
|
11
|
+
* `undefined` for "no snapshot").
|
|
12
|
+
*
|
|
13
|
+
* Note: the factory form is detected via `typeof initialPayload === 'function'`. If `TEventData` is
|
|
14
|
+
* itself a callable type, a value-form payload would be misclassified as a factory — wrap such a value
|
|
15
|
+
* in an array (`[value]`) to disambiguate. GraphQL event payloads are objects, so this is not a concern
|
|
16
|
+
* in practice.
|
|
17
|
+
*/
|
|
18
|
+
export type InitialPayload<TEventData> = TEventData | TEventData[] | InitialPayloadFactory<TEventData>;
|
|
1
19
|
/**
|
|
2
20
|
* Wraps an async iterator with options for:
|
|
3
21
|
* - returning an initial payload (e.g. current state)
|
|
@@ -8,16 +26,29 @@
|
|
|
8
26
|
*
|
|
9
27
|
* Eagerly ending iteration when a final event ensures subscriptions do not continue unnecessarily.
|
|
10
28
|
*
|
|
29
|
+
* ### Avoiding the subscribe-before-snapshot race
|
|
30
|
+
*
|
|
31
|
+
* `graphql-subscriptions`' iterator subscribes to its channel lazily, on its first `next()`. If you read the
|
|
32
|
+
* snapshot (the initial payload) *before* constructing/pulling the iterator, any event published between the
|
|
33
|
+
* snapshot read and the subscription going live is lost — and missing from the snapshot too. To close that
|
|
34
|
+
* window, pass `initialPayload` as a **factory** (`() => ... | Promise<...>`). When a factory is supplied this
|
|
35
|
+
* helper eagerly pulls the wrapped iterator once (establishing the subscription) *before* invoking the factory
|
|
36
|
+
* to read the snapshot; events fired during the snapshot read are then buffered by the wrapped iterator instead
|
|
37
|
+
* of dropped, and are delivered immediately after the snapshot.
|
|
38
|
+
*
|
|
11
39
|
* @param iterator The async iterator to wrap.
|
|
12
|
-
* @param initialPayload An optional initial payload to return on the first iteration. May be a single event
|
|
40
|
+
* @param initialPayload An optional initial payload to return on the first iteration. May be a single event, an
|
|
41
|
+
* array, or a factory (`() => event | event[] | Promise<event | event[]>`). Prefer the factory form for the
|
|
42
|
+
* "snapshot then stream" pattern so the subscription is established before the snapshot is read.
|
|
13
43
|
* @param eventIsFinal An optional function to determine if an event is final (and iteration should end).
|
|
14
44
|
* The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which
|
|
15
|
-
* the wrapped iterator is closed
|
|
16
|
-
* is
|
|
45
|
+
* the wrapped iterator is closed (`wrapped.return()`). In the value form it is closed without ever being pulled; in
|
|
46
|
+
* the factory form the single eager pull used to establish the subscription is discarded on teardown. For events
|
|
47
|
+
* from the wrapped iterator, every event is checked.
|
|
17
48
|
* @returns An async iterator for the event data.
|
|
18
49
|
*/
|
|
19
50
|
export declare function wrapSubscriptionIterator<TEventData>({ iterator: wrapped, initialPayload, eventIsFinal, }: {
|
|
20
51
|
iterator: AsyncIterableIterator<TEventData>;
|
|
21
|
-
initialPayload?: TEventData
|
|
52
|
+
initialPayload?: InitialPayload<TEventData>;
|
|
22
53
|
eventIsFinal?: (event: TEventData) => boolean;
|
|
23
54
|
}): AsyncIterator<TEventData> & AsyncIterable<TEventData>;
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A function that lazily produces an initial payload. Provided in place of a plain value when the
|
|
3
|
+
* caller needs the wrapped iterator's subscription to be established *before* the payload is read.
|
|
4
|
+
*
|
|
5
|
+
* May return `undefined` (or a promise of it) to indicate "no snapshot" — e.g. a repository
|
|
6
|
+
* `findOne` that yields nothing — in which case iteration proceeds straight to the wrapped stream.
|
|
7
|
+
*/
|
|
8
|
+
export type InitialPayloadFactory<TEventData> = () => TEventData | TEventData[] | undefined | Promise<TEventData | TEventData[] | undefined>;
|
|
9
|
+
/**
|
|
10
|
+
* An initial payload: a single event, an array of events, or a factory that produces either (or
|
|
11
|
+
* `undefined` for "no snapshot").
|
|
12
|
+
*
|
|
13
|
+
* Note: the factory form is detected via `typeof initialPayload === 'function'`. If `TEventData` is
|
|
14
|
+
* itself a callable type, a value-form payload would be misclassified as a factory — wrap such a value
|
|
15
|
+
* in an array (`[value]`) to disambiguate. GraphQL event payloads are objects, so this is not a concern
|
|
16
|
+
* in practice.
|
|
17
|
+
*/
|
|
18
|
+
export type InitialPayload<TEventData> = TEventData | TEventData[] | InitialPayloadFactory<TEventData>;
|
|
1
19
|
/**
|
|
2
20
|
* Wraps an async iterator with options for:
|
|
3
21
|
* - returning an initial payload (e.g. current state)
|
|
@@ -8,16 +26,29 @@
|
|
|
8
26
|
*
|
|
9
27
|
* Eagerly ending iteration when a final event ensures subscriptions do not continue unnecessarily.
|
|
10
28
|
*
|
|
29
|
+
* ### Avoiding the subscribe-before-snapshot race
|
|
30
|
+
*
|
|
31
|
+
* `graphql-subscriptions`' iterator subscribes to its channel lazily, on its first `next()`. If you read the
|
|
32
|
+
* snapshot (the initial payload) *before* constructing/pulling the iterator, any event published between the
|
|
33
|
+
* snapshot read and the subscription going live is lost — and missing from the snapshot too. To close that
|
|
34
|
+
* window, pass `initialPayload` as a **factory** (`() => ... | Promise<...>`). When a factory is supplied this
|
|
35
|
+
* helper eagerly pulls the wrapped iterator once (establishing the subscription) *before* invoking the factory
|
|
36
|
+
* to read the snapshot; events fired during the snapshot read are then buffered by the wrapped iterator instead
|
|
37
|
+
* of dropped, and are delivered immediately after the snapshot.
|
|
38
|
+
*
|
|
11
39
|
* @param iterator The async iterator to wrap.
|
|
12
|
-
* @param initialPayload An optional initial payload to return on the first iteration. May be a single event
|
|
40
|
+
* @param initialPayload An optional initial payload to return on the first iteration. May be a single event, an
|
|
41
|
+
* array, or a factory (`() => event | event[] | Promise<event | event[]>`). Prefer the factory form for the
|
|
42
|
+
* "snapshot then stream" pattern so the subscription is established before the snapshot is read.
|
|
13
43
|
* @param eventIsFinal An optional function to determine if an event is final (and iteration should end).
|
|
14
44
|
* The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which
|
|
15
|
-
* the wrapped iterator is closed
|
|
16
|
-
* is
|
|
45
|
+
* the wrapped iterator is closed (`wrapped.return()`). In the value form it is closed without ever being pulled; in
|
|
46
|
+
* the factory form the single eager pull used to establish the subscription is discarded on teardown. For events
|
|
47
|
+
* from the wrapped iterator, every event is checked.
|
|
17
48
|
* @returns An async iterator for the event data.
|
|
18
49
|
*/
|
|
19
50
|
export declare function wrapSubscriptionIterator<TEventData>({ iterator: wrapped, initialPayload, eventIsFinal, }: {
|
|
20
51
|
iterator: AsyncIterableIterator<TEventData>;
|
|
21
|
-
initialPayload?: TEventData
|
|
52
|
+
initialPayload?: InitialPayload<TEventData>;
|
|
22
53
|
eventIsFinal?: (event: TEventData) => boolean;
|
|
23
54
|
}): AsyncIterator<TEventData> & AsyncIterable<TEventData>;
|
|
@@ -10,30 +10,74 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Eagerly ending iteration when a final event ensures subscriptions do not continue unnecessarily.
|
|
12
12
|
*
|
|
13
|
+
* ### Avoiding the subscribe-before-snapshot race
|
|
14
|
+
*
|
|
15
|
+
* `graphql-subscriptions`' iterator subscribes to its channel lazily, on its first `next()`. If you read the
|
|
16
|
+
* snapshot (the initial payload) *before* constructing/pulling the iterator, any event published between the
|
|
17
|
+
* snapshot read and the subscription going live is lost — and missing from the snapshot too. To close that
|
|
18
|
+
* window, pass `initialPayload` as a **factory** (`() => ... | Promise<...>`). When a factory is supplied this
|
|
19
|
+
* helper eagerly pulls the wrapped iterator once (establishing the subscription) *before* invoking the factory
|
|
20
|
+
* to read the snapshot; events fired during the snapshot read are then buffered by the wrapped iterator instead
|
|
21
|
+
* of dropped, and are delivered immediately after the snapshot.
|
|
22
|
+
*
|
|
13
23
|
* @param iterator The async iterator to wrap.
|
|
14
|
-
* @param initialPayload An optional initial payload to return on the first iteration. May be a single event
|
|
24
|
+
* @param initialPayload An optional initial payload to return on the first iteration. May be a single event, an
|
|
25
|
+
* array, or a factory (`() => event | event[] | Promise<event | event[]>`). Prefer the factory form for the
|
|
26
|
+
* "snapshot then stream" pattern so the subscription is established before the snapshot is read.
|
|
15
27
|
* @param eventIsFinal An optional function to determine if an event is final (and iteration should end).
|
|
16
28
|
* The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which
|
|
17
|
-
* the wrapped iterator is closed
|
|
18
|
-
* is
|
|
29
|
+
* the wrapped iterator is closed (`wrapped.return()`). In the value form it is closed without ever being pulled; in
|
|
30
|
+
* the factory form the single eager pull used to establish the subscription is discarded on teardown. For events
|
|
31
|
+
* from the wrapped iterator, every event is checked.
|
|
19
32
|
* @returns An async iterator for the event data.
|
|
20
33
|
*/
|
|
21
34
|
function wrapSubscriptionIterator({ iterator: wrapped, initialPayload, eventIsFinal, }) {
|
|
22
35
|
let done = false;
|
|
23
|
-
const
|
|
36
|
+
const toQueue = (payload) => payload === undefined ? [] : Array.isArray(payload) ? [...payload] : [payload];
|
|
37
|
+
// Factory form: the caller wants the channel subscription established BEFORE the initial payload
|
|
38
|
+
// (typically a DB snapshot) is read. The wrapped iterator subscribes on its first `next()`, so pull it
|
|
39
|
+
// eagerly *now*. The resulting event is buffered and delivered after the initial payload drains; the
|
|
40
|
+
// factory (and therefore the snapshot read) runs lazily on the first consumer `next()`, after the
|
|
41
|
+
// subscription is already kicked.
|
|
42
|
+
const isFactory = typeof initialPayload === 'function';
|
|
43
|
+
let eagerPull = isFactory ? wrapped.next() : undefined;
|
|
44
|
+
// Value form: build the queue eagerly, exactly as before. Factory form: resolved lazily on first next().
|
|
45
|
+
let initialQueue = isFactory ? undefined : toQueue(initialPayload);
|
|
46
|
+
// Hand back the eagerly-pulled (= already-subscribed) event the first time, then defer to wrapped.next().
|
|
47
|
+
const consumeWrapped = () => {
|
|
48
|
+
if (eagerPull) {
|
|
49
|
+
const pull = eagerPull;
|
|
50
|
+
eagerPull = undefined;
|
|
51
|
+
return pull;
|
|
52
|
+
}
|
|
53
|
+
return wrapped.next();
|
|
54
|
+
};
|
|
55
|
+
// Abandon the eager pull without leaking an unhandled rejection. Only reached via early teardown
|
|
56
|
+
// (return/throw before the buffered event is consumed) or the eventIsFinal short-circuit on the
|
|
57
|
+
// snapshot — neither consumes the buffered event, and wrapped.return() tears down the kicked subscription.
|
|
58
|
+
const discardEagerPull = () => {
|
|
59
|
+
if (eagerPull) {
|
|
60
|
+
eagerPull.catch(() => { });
|
|
61
|
+
eagerPull = undefined;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
24
64
|
const iterator = {
|
|
25
65
|
next: async () => {
|
|
26
66
|
if (done)
|
|
27
67
|
return { done: true, value: undefined };
|
|
68
|
+
if (initialQueue === undefined) {
|
|
69
|
+
initialQueue = toQueue(await initialPayload());
|
|
70
|
+
}
|
|
28
71
|
if (initialQueue.length > 0) {
|
|
29
72
|
const value = initialQueue.shift();
|
|
30
73
|
if (initialQueue.length === 0 && eventIsFinal && eventIsFinal(value)) {
|
|
31
74
|
done = true;
|
|
75
|
+
discardEagerPull(); // tear down the redundant eager subscription, if any
|
|
32
76
|
await wrapped.return?.();
|
|
33
77
|
}
|
|
34
78
|
return { done: false, value };
|
|
35
79
|
}
|
|
36
|
-
const next = await
|
|
80
|
+
const next = await consumeWrapped();
|
|
37
81
|
if (!next.done && eventIsFinal && eventIsFinal(next.value)) {
|
|
38
82
|
done = true;
|
|
39
83
|
await wrapped.return?.();
|
|
@@ -42,14 +86,18 @@ function wrapSubscriptionIterator({ iterator: wrapped, initialPayload, eventIsFi
|
|
|
42
86
|
},
|
|
43
87
|
return: async (value) => {
|
|
44
88
|
done = true;
|
|
45
|
-
initialQueue
|
|
89
|
+
if (initialQueue)
|
|
90
|
+
initialQueue.length = 0;
|
|
91
|
+
discardEagerPull();
|
|
46
92
|
if (wrapped.return)
|
|
47
93
|
return wrapped.return(value);
|
|
48
94
|
return { done: true, value: value };
|
|
49
95
|
},
|
|
50
96
|
throw: async (err) => {
|
|
51
97
|
done = true;
|
|
52
|
-
initialQueue
|
|
98
|
+
if (initialQueue)
|
|
99
|
+
initialQueue.length = 0;
|
|
100
|
+
discardEagerPull();
|
|
53
101
|
if (wrapped.throw)
|
|
54
102
|
return wrapped.throw(err);
|
|
55
103
|
throw err;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription-iterator.js","sources":["../../src/subscriptions/subscription-iterator.ts"],"sourcesContent":["/**\n * Wraps an async iterator with options for:\n * - returning an initial payload (e.g. current state)\n * - eagerly ending iteration when a final event is received\n *\n * Providing an initial payload helps avoid race conditions when clients subscribe to events but miss recent state changes,\n * by providing an initial payload to be immediately returned, before subsequent events stream in.\n *\n * Eagerly ending iteration when a final event ensures subscriptions do not continue unnecessarily.\n *\n * @param iterator The async iterator to wrap.\n * @param initialPayload An optional initial payload to return on the first iteration. May be a single event or
|
|
1
|
+
{"version":3,"file":"subscription-iterator.js","sources":["../../src/subscriptions/subscription-iterator.ts"],"sourcesContent":["/**\n * A function that lazily produces an initial payload. Provided in place of a plain value when the\n * caller needs the wrapped iterator's subscription to be established *before* the payload is read.\n *\n * May return `undefined` (or a promise of it) to indicate \"no snapshot\" — e.g. a repository\n * `findOne` that yields nothing — in which case iteration proceeds straight to the wrapped stream.\n */\nexport type InitialPayloadFactory<TEventData> = () => TEventData | TEventData[] | undefined | Promise<TEventData | TEventData[] | undefined>\n\n/**\n * An initial payload: a single event, an array of events, or a factory that produces either (or\n * `undefined` for \"no snapshot\").\n *\n * Note: the factory form is detected via `typeof initialPayload === 'function'`. If `TEventData` is\n * itself a callable type, a value-form payload would be misclassified as a factory — wrap such a value\n * in an array (`[value]`) to disambiguate. GraphQL event payloads are objects, so this is not a concern\n * in practice.\n */\nexport type InitialPayload<TEventData> = TEventData | TEventData[] | InitialPayloadFactory<TEventData>\n\n/**\n * Wraps an async iterator with options for:\n * - returning an initial payload (e.g. current state)\n * - eagerly ending iteration when a final event is received\n *\n * Providing an initial payload helps avoid race conditions when clients subscribe to events but miss recent state changes,\n * by providing an initial payload to be immediately returned, before subsequent events stream in.\n *\n * Eagerly ending iteration when a final event ensures subscriptions do not continue unnecessarily.\n *\n * ### Avoiding the subscribe-before-snapshot race\n *\n * `graphql-subscriptions`' iterator subscribes to its channel lazily, on its first `next()`. If you read the\n * snapshot (the initial payload) *before* constructing/pulling the iterator, any event published between the\n * snapshot read and the subscription going live is lost — and missing from the snapshot too. To close that\n * window, pass `initialPayload` as a **factory** (`() => ... | Promise<...>`). When a factory is supplied this\n * helper eagerly pulls the wrapped iterator once (establishing the subscription) *before* invoking the factory\n * to read the snapshot; events fired during the snapshot read are then buffered by the wrapped iterator instead\n * of dropped, and are delivered immediately after the snapshot.\n *\n * @param iterator The async iterator to wrap.\n * @param initialPayload An optional initial payload to return on the first iteration. May be a single event, an\n * array, or a factory (`() => event | event[] | Promise<event | event[]>`). Prefer the factory form for the\n * \"snapshot then stream\" pattern so the subscription is established before the snapshot is read.\n * @param eventIsFinal An optional function to determine if an event is final (and iteration should end).\n * The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which\n * the wrapped iterator is closed (`wrapped.return()`). In the value form it is closed without ever being pulled; in\n * the factory form the single eager pull used to establish the subscription is discarded on teardown. For events\n * from the wrapped iterator, every event is checked.\n * @returns An async iterator for the event data.\n */\nexport function wrapSubscriptionIterator<TEventData>({\n iterator: wrapped,\n initialPayload,\n eventIsFinal,\n}: {\n iterator: AsyncIterableIterator<TEventData>\n initialPayload?: InitialPayload<TEventData>\n eventIsFinal?: (event: TEventData) => boolean\n}): AsyncIterator<TEventData> & AsyncIterable<TEventData> {\n let done = false\n\n const toQueue = (payload: TEventData | TEventData[] | undefined): TEventData[] =>\n payload === undefined ? [] : Array.isArray(payload) ? [...payload] : [payload]\n\n // Factory form: the caller wants the channel subscription established BEFORE the initial payload\n // (typically a DB snapshot) is read. The wrapped iterator subscribes on its first `next()`, so pull it\n // eagerly *now*. The resulting event is buffered and delivered after the initial payload drains; the\n // factory (and therefore the snapshot read) runs lazily on the first consumer `next()`, after the\n // subscription is already kicked.\n const isFactory = typeof initialPayload === 'function'\n let eagerPull: Promise<IteratorResult<TEventData>> | undefined = isFactory ? wrapped.next() : undefined\n\n // Value form: build the queue eagerly, exactly as before. Factory form: resolved lazily on first next().\n let initialQueue: TEventData[] | undefined = isFactory ? undefined : toQueue(initialPayload as TEventData | TEventData[] | undefined)\n\n // Hand back the eagerly-pulled (= already-subscribed) event the first time, then defer to wrapped.next().\n const consumeWrapped = (): Promise<IteratorResult<TEventData>> => {\n if (eagerPull) {\n const pull = eagerPull\n eagerPull = undefined\n return pull\n }\n return wrapped.next()\n }\n\n // Abandon the eager pull without leaking an unhandled rejection. Only reached via early teardown\n // (return/throw before the buffered event is consumed) or the eventIsFinal short-circuit on the\n // snapshot — neither consumes the buffered event, and wrapped.return() tears down the kicked subscription.\n const discardEagerPull = () => {\n if (eagerPull) {\n eagerPull.catch(() => {})\n eagerPull = undefined\n }\n }\n\n const iterator: AsyncIterator<TEventData> = {\n next: async () => {\n if (done) return { done: true, value: undefined }\n\n if (initialQueue === undefined) {\n initialQueue = toQueue(await (initialPayload as InitialPayloadFactory<TEventData>)())\n }\n\n if (initialQueue.length > 0) {\n const value = initialQueue.shift() as TEventData\n if (initialQueue.length === 0 && eventIsFinal && eventIsFinal(value)) {\n done = true\n discardEagerPull() // tear down the redundant eager subscription, if any\n await wrapped.return?.()\n }\n return { done: false, value }\n }\n\n const next = await consumeWrapped()\n if (!next.done && eventIsFinal && eventIsFinal(next.value)) {\n done = true\n await wrapped.return?.()\n }\n return next\n },\n return: async (value) => {\n done = true\n if (initialQueue) initialQueue.length = 0\n discardEagerPull()\n if (wrapped.return) return wrapped.return(value)\n return { done: true, value: value as TEventData }\n },\n throw: async (err) => {\n done = true\n if (initialQueue) initialQueue.length = 0\n discardEagerPull()\n if (wrapped.throw) return wrapped.throw(err)\n throw err\n },\n }\n\n return { ...iterator, [Symbol.asyncIterator]: () => iterator }\n}\n"],"names":[],"mappings":"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"}
|