import { describe, it } from 'node:test';
import { assert, expect } from 'chai';
import { expectEvents } from '../../__testUtils__/expectEvents.ts';
import { expectNoTracingActivity } from '../../__testUtils__/expectNoTracingActivity.ts';
import { expectToThrow } from '../../__testUtils__/expectToThrow.ts';
import { getTracingChannel } from '../../__testUtils__/getTracingChannel.ts';
import { isAsyncIterable } from '../../jsutils/isAsyncIterable.ts';
import type { OperationDefinitionNode } from '../../language/ast.ts';
import { OperationTypeNode } from '../../language/ast.ts';
import { parse } from '../../language/parser.ts';
import type { GraphQLSchema } from '../../type/schema.ts';
import { buildSchema } from '../../utilities/buildASTSchema.ts';
import {
createSourceEventStream,
executeSubscriptionEvent,
mapSourceToResponseEvent,
subscribe,
validateSubscriptionArgs,
} from '../execute.ts';
const schema = buildSchema(`
type Query {
dummy: String
}
type Subscription {
tick: String
}
`);
const subscribeChannel = getTracingChannel('graphql:subscribe');
const executeRootSelectionSetChannel = getTracingChannel(
'graphql:execute:rootSelectionSet',
);
async function* twoTicks(): AsyncIterable<{ tick: string }> {
await Promise.resolve();
yield { tick: 'one' };
yield { tick: 'two' };
}
describe('subscribe diagnostics channel', () => {
it('emits start and end for a synchronous subscription setup', async () => {
const document = parse('subscription S($tick: String) { tick }');
const variableValues = { tick: 'ignored by the field' };
await expectEvents(
subscribeChannel,
async () => {
const subscription = await subscribe({
schema,
document,
rootValue: { tick: twoTicks },
variableValues,
});
assert(isAsyncIterable(subscription));
const returned = subscription.return?.();
if (returned !== undefined) {
await returned;
}
return subscription;
},
(result) => [
{
channel: 'start',
context: {
schema,
document,
rawVariableValues: variableValues,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
},
},
{
channel: 'end',
context: {
schema,
document,
rawVariableValues: variableValues,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
result,
},
},
],
);
});
it('emits the full async lifecycle when subscribe resolver returns a promise', async () => {
const document = parse('subscription { tick }');
await expectEvents(
subscribeChannel,
async () => {
const subscription = await subscribe({
schema,
document,
rootValue: {
tick: (): Promise<AsyncIterable<{ tick: string }>> =>
Promise.resolve(twoTicks()),
},
});
assert(isAsyncIterable(subscription));
const returned = subscription.return?.();
if (returned !== undefined) {
await returned;
}
return subscription;
},
(result) => [
{
channel: 'start',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.SUBSCRIPTION,
},
},
{
channel: 'end',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.SUBSCRIPTION,
},
},
{
channel: 'asyncStart',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.SUBSCRIPTION,
result,
},
},
{
channel: 'asyncEnd',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: OperationTypeNode.SUBSCRIPTION,
result,
},
},
],
);
});
it('emits execute root selection set events for each event with the default per-event executor', async () => {
const document = parse('subscription S($tick: String) { tick }');
const operation = document.definitions[0] as OperationDefinitionNode;
const variableValues = { tick: 'ignored by the field' };
await expectEvents(
executeRootSelectionSetChannel,
async () => {
const subscription = await subscribe({
schema,
document,
rootValue: { tick: twoTicks },
variableValues,
});
assert(isAsyncIterable(subscription));
const firstResult = await subscription.next();
expect(firstResult).to.deep.equal({
done: false,
value: { data: { tick: 'one' } },
});
assert(!firstResult.done, 'Expected first subscription event.');
const secondResult = await subscription.next();
expect(secondResult).to.deep.equal({
done: false,
value: { data: { tick: 'two' } },
});
assert(!secondResult.done, 'Expected second subscription event.');
const returned = subscription.return?.();
if (returned !== undefined) {
await returned;
}
return [firstResult.value, secondResult.value] as const;
},
([firstResult, secondResult]) => [
{
channel: 'start',
context: {
schema,
document,
operation,
rawVariableValues: variableValues,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
},
},
{
channel: 'end',
context: {
schema,
document,
operation,
rawVariableValues: variableValues,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
result: firstResult,
},
},
{
channel: 'start',
context: {
schema,
document,
operation,
rawVariableValues: variableValues,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
},
},
{
channel: 'end',
context: {
schema,
document,
operation,
rawVariableValues: variableValues,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
result: secondResult,
},
},
],
);
});
it('emits execute root selection set events for each event with a custom per-event executor', async () => {
const document = parse('subscription S($tick: String) { tick }');
const operation = document.definitions[0] as OperationDefinitionNode;
const variableValues = { tick: 'ignored by the field' };
await expectEvents(
executeRootSelectionSetChannel,
async () => {
const validatedArgs = validateSubscriptionArgs({
schema,
document,
rootValue: { tick: twoTicks },
variableValues,
});
if (!('schema' in validatedArgs)) {
throw new Error('Unexpected validation errors');
}
const sourceEventStream = await createSourceEventStream(validatedArgs);
assert(isAsyncIterable(sourceEventStream));
const customExecutor: typeof executeSubscriptionEvent = (args) =>
executeSubscriptionEvent(args);
const responseStream = mapSourceToResponseEvent(
validatedArgs,
sourceEventStream,
customExecutor,
);
const firstResult = await responseStream.next();
expect(firstResult).to.deep.equal({
done: false,
value: { data: { tick: 'one' } },
});
assert(!firstResult.done, 'Expected first subscription event.');
const secondResult = await responseStream.next();
expect(secondResult).to.deep.equal({
done: false,
value: { data: { tick: 'two' } },
});
assert(!secondResult.done, 'Expected second subscription event.');
const returned = responseStream.return?.();
if (returned !== undefined) {
await returned;
}
return [firstResult.value, secondResult.value] as const;
},
([firstResult, secondResult]) => [
{
channel: 'start',
context: {
schema,
document,
operation,
rawVariableValues: variableValues,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
},
},
{
channel: 'end',
context: {
schema,
document,
operation,
rawVariableValues: variableValues,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
result: firstResult,
},
},
{
channel: 'start',
context: {
schema,
document,
operation,
rawVariableValues: variableValues,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
},
},
{
channel: 'end',
context: {
schema,
document,
operation,
rawVariableValues: variableValues,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
result: secondResult,
},
},
],
);
});
it('emits only start and end for a synchronous validation failure', async () => {
const document = parse('fragment F on Subscription { tick }');
await expectEvents(
subscribeChannel,
async () => {
const result = await subscribe({ schema, document });
expect(result).to.have.property('errors');
return result;
},
(result) => [
{
channel: 'start',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: undefined,
},
},
{
channel: 'end',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: undefined,
operationType: undefined,
result,
},
},
],
);
});
it('emits start, error, and end when subscribe throws synchronously', async () => {
const document = parse('subscription S { tick }');
const invalidSchema = {} as GraphQLSchema;
await expectEvents(
subscribeChannel,
() => expectToThrow(() => subscribe({ schema: invalidSchema, document })),
(error) => [
{
channel: 'start',
context: {
schema: invalidSchema,
document,
rawVariableValues: undefined,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
},
},
{
channel: 'error',
context: {
schema: invalidSchema,
document,
rawVariableValues: undefined,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
error,
},
},
{
channel: 'end',
context: {
schema: invalidSchema,
document,
rawVariableValues: undefined,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
error,
},
},
],
);
});
it('emits full async lifecycle when subscribe resolver rejects and subscribe resolves to an error result', async () => {
const document = parse('subscription S { tick }');
const error = new Error('subscribe-boom');
await expectEvents(
subscribeChannel,
async () => {
const result = await subscribe({
schema,
document,
rootValue: {
tick: () => Promise.reject(error),
},
});
expect(result).to.have.property('errors');
return result;
},
(result) => [
{
channel: 'start',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
},
},
{
channel: 'end',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
},
},
{
channel: 'asyncStart',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
result,
},
},
{
channel: 'asyncEnd',
context: {
schema,
document,
rawVariableValues: undefined,
operationName: 'S',
operationType: OperationTypeNode.SUBSCRIPTION,
result,
},
},
],
);
});
it('does not call tracing methods when no subscribers are attached', async () => {
const document = parse('subscription { tick }');
await expectNoTracingActivity(subscribeChannel, async () => {
const resolved = await subscribe({
schema,
document,
rootValue: { tick: twoTicks },
});
assert(isAsyncIterable(resolved));
const returned = resolved.return?.();
if (returned !== undefined) {
await returned;
}
});
});
});