graphile-realtime-test 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,116 @@
1
+ import { getConnections as getPgConnections } from 'pgsql-test';
2
+ import { makeSchema } from 'graphile-build';
3
+ import { defaultPreset as graphileBuildDefaultPreset } from 'graphile-build';
4
+ import { defaultPreset as graphileBuildPgDefaultPreset } from 'graphile-build-pg';
5
+ import { makePgService } from 'postgraphile/adaptors/pg';
6
+ import { createRealtimeSubscriptionsPlugin } from 'graphile-realtime-subscriptions';
7
+ import { makeRealtimeSmartTagsPlugin } from './smart-tags.js';
8
+ import { createWsTestServer } from './ws-server.js';
9
+ import { nextEvent, collectWsEvents, delay } from './ws-helpers.js';
10
+ import { notify, notifyChange, notifyInvalidate } from './notify.js';
11
+ // --- Types ---
12
+ const MinimalPreset = {
13
+ extends: [graphileBuildDefaultPreset, graphileBuildPgDefaultPreset],
14
+ disablePlugins: ['NodePlugin'],
15
+ };
16
+ // --- Implementation ---
17
+ export async function getConnections(input, seedAdapters) {
18
+ const { schemas, preset: userPreset, smartTags, realtimeTables, realtimeOptions, buildPgSettings, } = input;
19
+ // 1. Database connections (pgsql-test layer)
20
+ const conn = await getPgConnections(input, seedAdapters);
21
+ const { pg, db, teardown: dbTeardown } = conn;
22
+ const pgPool = conn.manager.getPool(conn.pg.config);
23
+ // 2. Smart tags
24
+ const smartTagsMap = {
25
+ ...(smartTags ?? {}),
26
+ };
27
+ for (const table of realtimeTables ?? []) {
28
+ if (!smartTagsMap[table]) {
29
+ smartTagsMap[table] = {};
30
+ }
31
+ smartTagsMap[table].realtime = true;
32
+ }
33
+ const smartTagsPlugin = makeRealtimeSmartTagsPlugin(smartTagsMap);
34
+ const realtimePlugin = createRealtimeSubscriptionsPlugin(realtimeOptions);
35
+ // 3. Graphile schema (graphile-test layer)
36
+ const pgService = makePgService({
37
+ pool: pgPool,
38
+ schemas,
39
+ });
40
+ const completePreset = {
41
+ extends: [
42
+ MinimalPreset,
43
+ ...(userPreset?.extends ?? []),
44
+ ],
45
+ ...(userPreset?.disablePlugins && { disablePlugins: userPreset.disablePlugins }),
46
+ plugins: [
47
+ ...(userPreset?.plugins ?? []),
48
+ smartTagsPlugin,
49
+ realtimePlugin,
50
+ ],
51
+ ...(userPreset?.schema && { schema: userPreset.schema }),
52
+ ...(userPreset?.grafast && { grafast: userPreset.grafast }),
53
+ pgServices: [pgService],
54
+ };
55
+ const result = await makeSchema(completePreset);
56
+ const schema = result.schema;
57
+ const resolvedPreset = result.resolvedPreset;
58
+ const pgSubscriber = pgService.pgSubscriber;
59
+ const pgSubscriberKey = pgService.pgSubscriberKey ?? 'pgSubscriber';
60
+ // 4. WebSocket server (realtime layer)
61
+ const wsServer = await createWsTestServer({
62
+ schema,
63
+ resolvedPreset,
64
+ pgSubscriber,
65
+ pgSubscriberKey,
66
+ buildPgSettings,
67
+ });
68
+ await delay(300);
69
+ // 5. WS handle
70
+ const ws = {
71
+ serverUrl: wsServer.serverUrl,
72
+ createClient: (connectionParams) => wsServer.createClient(connectionParams),
73
+ nextEvent: (client, query, variables, timeoutMs) => nextEvent(client, query, variables, timeoutMs),
74
+ collectEvents: (client, query, variables) => collectWsEvents(client, query, variables),
75
+ dispose: () => wsServer.dispose(),
76
+ };
77
+ // 6. Notify helpers
78
+ const defaultSchema = schemas[0] ?? 'public';
79
+ const notifyClient = pg.client;
80
+ // 7. Teardown (idempotent)
81
+ let tornDown = false;
82
+ const teardown = async () => {
83
+ if (tornDown)
84
+ return;
85
+ tornDown = true;
86
+ // Order: clients -> server -> subscriber -> service -> pool -> db
87
+ await wsServer.dispose();
88
+ if (pgSubscriber && typeof pgSubscriber.release === 'function') {
89
+ await pgSubscriber.release();
90
+ }
91
+ if (pgService && typeof pgService.release === 'function') {
92
+ await pgService.release();
93
+ }
94
+ await dbTeardown();
95
+ };
96
+ return {
97
+ pg,
98
+ db,
99
+ schema,
100
+ resolvedPreset,
101
+ pgPool,
102
+ pgService,
103
+ pgSubscriber,
104
+ ws,
105
+ async notifyChange(table, operation, rowIds, tableSchema) {
106
+ await notifyChange(notifyClient, tableSchema ?? defaultSchema, table, operation, rowIds);
107
+ },
108
+ async notifyInvalidate(table, tableSchema) {
109
+ await notifyInvalidate(notifyClient, tableSchema ?? defaultSchema, table);
110
+ },
111
+ async notify(table, payload, tableSchema) {
112
+ await notify(notifyClient, tableSchema ?? defaultSchema, table, payload);
113
+ },
114
+ teardown,
115
+ };
116
+ }
package/esm/index.js CHANGED
@@ -2,3 +2,6 @@ export { createRealtimeTestContext } from './context.js';
2
2
  export { makeRealtimeSmartTagsPlugin } from './smart-tags.js';
3
3
  export { subscribe, waitForEvent, collectEvents, } from './subscribe.js';
4
4
  export { notify, notifyChange, notifyInvalidate, buildPayload, buildInvalidatePayload, } from './notify.js';
5
+ export { nextEvent, collectWsEvents, delay } from './ws-helpers.js';
6
+ export { createWsTestServer } from './ws-server.js';
7
+ export { getConnections } from './get-connections.js';
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Collect the next event from a graphql-ws WebSocket subscription.
3
+ *
4
+ * Returns a promise that resolves with the first `next` payload's `data`,
5
+ * or rejects on error / timeout. The subscription is automatically
6
+ * unsubscribed after the first event.
7
+ */
8
+ export function nextEvent(client, query, variables, timeoutMs = 10000) {
9
+ return new Promise((resolve, reject) => {
10
+ const timer = setTimeout(() => {
11
+ unsubscribe();
12
+ reject(new Error(`nextEvent timed out after ${timeoutMs}ms`));
13
+ }, timeoutMs);
14
+ const unsubscribe = client.subscribe({ query, variables }, {
15
+ next(value) {
16
+ clearTimeout(timer);
17
+ unsubscribe();
18
+ resolve(value.data);
19
+ },
20
+ error(err) {
21
+ clearTimeout(timer);
22
+ unsubscribe();
23
+ reject(Array.isArray(err) ? err[0] : err);
24
+ },
25
+ complete() {
26
+ clearTimeout(timer);
27
+ reject(new Error('Subscription completed without yielding a value'));
28
+ },
29
+ });
30
+ });
31
+ }
32
+ /**
33
+ * Subscribe and collect events into an array until `unsubscribe()` is called.
34
+ *
35
+ * Useful for assertions that need to inspect multiple events after the fact.
36
+ */
37
+ export function collectWsEvents(client, query, variables) {
38
+ const events = [];
39
+ const unsubscribe = client.subscribe({ query, variables }, {
40
+ next(value) {
41
+ events.push(value.data);
42
+ },
43
+ error() { },
44
+ complete() { },
45
+ });
46
+ return { events, unsubscribe };
47
+ }
48
+ export const delay = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -0,0 +1,73 @@
1
+ import { createServer } from 'node:http';
2
+ import { subscribe as grafastSubscribe } from 'grafast';
3
+ import { createClient } from 'graphql-ws';
4
+ import { useServer } from 'graphql-ws/use/ws';
5
+ import { WebSocketServer, WebSocket } from 'ws';
6
+ export async function createWsTestServer(input) {
7
+ const { schema, resolvedPreset, pgSubscriber, pgSubscriberKey = 'pgSubscriber', buildPgSettings, } = input;
8
+ const httpServer = createServer();
9
+ const wss = new WebSocketServer({ server: httpServer, path: '/graphql' });
10
+ const serverCleanup = useServer({
11
+ schema,
12
+ context: async (ctx) => ({
13
+ connectionParams: ctx.connectionParams,
14
+ }),
15
+ subscribe: async (args) => {
16
+ const params = args.contextValue?.connectionParams;
17
+ const contextValue = {
18
+ [pgSubscriberKey]: pgSubscriber,
19
+ ...(typeof args.contextValue === 'object' && args.contextValue !== null
20
+ ? args.contextValue
21
+ : {}),
22
+ };
23
+ if (buildPgSettings && params) {
24
+ const pgSettings = buildPgSettings(params);
25
+ if (pgSettings) {
26
+ contextValue['pgSettings'] = pgSettings;
27
+ }
28
+ }
29
+ const result = await grafastSubscribe({
30
+ schema: args.schema,
31
+ document: args.document,
32
+ variableValues: args.variableValues,
33
+ contextValue,
34
+ resolvedPreset,
35
+ });
36
+ return result;
37
+ },
38
+ }, wss);
39
+ await new Promise((resolve) => {
40
+ httpServer.listen(0, '127.0.0.1', () => resolve());
41
+ });
42
+ const addr = httpServer.address();
43
+ const serverUrl = `ws://127.0.0.1:${addr.port}/graphql`;
44
+ const clients = [];
45
+ return {
46
+ serverUrl,
47
+ httpServer,
48
+ wss,
49
+ createClient(connectionParams) {
50
+ const client = createClient({
51
+ url: serverUrl,
52
+ webSocketImpl: WebSocket,
53
+ retryAttempts: 0,
54
+ connectionParams,
55
+ });
56
+ clients.push(client);
57
+ return client;
58
+ },
59
+ async dispose() {
60
+ for (const client of clients) {
61
+ try {
62
+ await client.dispose();
63
+ }
64
+ catch { /* ignore */ }
65
+ }
66
+ await serverCleanup.dispose();
67
+ wss.close();
68
+ if (httpServer.listening) {
69
+ await new Promise((resolve) => httpServer.close(() => resolve()));
70
+ }
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,45 @@
1
+ import type { GraphQLSchema } from 'graphql';
2
+ import type { GraphileConfig } from 'graphile-config';
3
+ import type { GetConnectionOpts } from 'pgsql-test';
4
+ import type { SeedAdapter } from 'pgsql-test/seed/types';
5
+ import type { PgTestClient } from 'pgsql-test/test-client';
6
+ import type { Pool } from 'pg';
7
+ import type { Client as GqlWsClient } from 'graphql-ws';
8
+ import { makePgService } from 'postgraphile/adaptors/pg';
9
+ export interface GetConnectionsInput extends GetConnectionOpts {
10
+ schemas: string[];
11
+ authRole?: string;
12
+ useRoot?: boolean;
13
+ preset?: GraphileConfig.Preset;
14
+ smartTags?: Record<string, Record<string, unknown>>;
15
+ realtimeTables?: string[];
16
+ realtimeOptions?: {
17
+ overflowThreshold?: number;
18
+ };
19
+ buildPgSettings?: (connectionParams: Record<string, string>) => Record<string, string> | undefined;
20
+ }
21
+ export interface WsHandle {
22
+ serverUrl: string;
23
+ createClient(connectionParams?: Record<string, unknown>): GqlWsClient;
24
+ nextEvent<T = Record<string, unknown>>(client: GqlWsClient, query: string, variables?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
25
+ collectEvents<T = Record<string, unknown>>(client: GqlWsClient, query: string, variables?: Record<string, unknown>): {
26
+ events: T[];
27
+ unsubscribe: () => void;
28
+ };
29
+ dispose(): Promise<void>;
30
+ }
31
+ export interface GetConnectionsResult {
32
+ pg: PgTestClient;
33
+ db: PgTestClient;
34
+ schema: GraphQLSchema;
35
+ resolvedPreset: GraphileConfig.ResolvedPreset;
36
+ pgPool: Pool;
37
+ pgService: ReturnType<typeof makePgService>;
38
+ pgSubscriber: unknown;
39
+ ws: WsHandle;
40
+ notifyChange(table: string, operation: 'INSERT' | 'UPDATE' | 'DELETE', rowIds: string[], schema?: string): Promise<void>;
41
+ notifyInvalidate(table: string, schema?: string): Promise<void>;
42
+ notify(table: string, payload: string, schema?: string): Promise<void>;
43
+ teardown(): Promise<void>;
44
+ }
45
+ export declare function getConnections(input: GetConnectionsInput, seedAdapters?: SeedAdapter[]): Promise<GetConnectionsResult>;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getConnections = getConnections;
4
+ const pgsql_test_1 = require("pgsql-test");
5
+ const graphile_build_1 = require("graphile-build");
6
+ const graphile_build_2 = require("graphile-build");
7
+ const graphile_build_pg_1 = require("graphile-build-pg");
8
+ const pg_1 = require("postgraphile/adaptors/pg");
9
+ const graphile_realtime_subscriptions_1 = require("graphile-realtime-subscriptions");
10
+ const smart_tags_js_1 = require("./smart-tags.js");
11
+ const ws_server_js_1 = require("./ws-server.js");
12
+ const ws_helpers_js_1 = require("./ws-helpers.js");
13
+ const notify_js_1 = require("./notify.js");
14
+ // --- Types ---
15
+ const MinimalPreset = {
16
+ extends: [graphile_build_2.defaultPreset, graphile_build_pg_1.defaultPreset],
17
+ disablePlugins: ['NodePlugin'],
18
+ };
19
+ // --- Implementation ---
20
+ async function getConnections(input, seedAdapters) {
21
+ const { schemas, preset: userPreset, smartTags, realtimeTables, realtimeOptions, buildPgSettings, } = input;
22
+ // 1. Database connections (pgsql-test layer)
23
+ const conn = await (0, pgsql_test_1.getConnections)(input, seedAdapters);
24
+ const { pg, db, teardown: dbTeardown } = conn;
25
+ const pgPool = conn.manager.getPool(conn.pg.config);
26
+ // 2. Smart tags
27
+ const smartTagsMap = {
28
+ ...(smartTags ?? {}),
29
+ };
30
+ for (const table of realtimeTables ?? []) {
31
+ if (!smartTagsMap[table]) {
32
+ smartTagsMap[table] = {};
33
+ }
34
+ smartTagsMap[table].realtime = true;
35
+ }
36
+ const smartTagsPlugin = (0, smart_tags_js_1.makeRealtimeSmartTagsPlugin)(smartTagsMap);
37
+ const realtimePlugin = (0, graphile_realtime_subscriptions_1.createRealtimeSubscriptionsPlugin)(realtimeOptions);
38
+ // 3. Graphile schema (graphile-test layer)
39
+ const pgService = (0, pg_1.makePgService)({
40
+ pool: pgPool,
41
+ schemas,
42
+ });
43
+ const completePreset = {
44
+ extends: [
45
+ MinimalPreset,
46
+ ...(userPreset?.extends ?? []),
47
+ ],
48
+ ...(userPreset?.disablePlugins && { disablePlugins: userPreset.disablePlugins }),
49
+ plugins: [
50
+ ...(userPreset?.plugins ?? []),
51
+ smartTagsPlugin,
52
+ realtimePlugin,
53
+ ],
54
+ ...(userPreset?.schema && { schema: userPreset.schema }),
55
+ ...(userPreset?.grafast && { grafast: userPreset.grafast }),
56
+ pgServices: [pgService],
57
+ };
58
+ const result = await (0, graphile_build_1.makeSchema)(completePreset);
59
+ const schema = result.schema;
60
+ const resolvedPreset = result.resolvedPreset;
61
+ const pgSubscriber = pgService.pgSubscriber;
62
+ const pgSubscriberKey = pgService.pgSubscriberKey ?? 'pgSubscriber';
63
+ // 4. WebSocket server (realtime layer)
64
+ const wsServer = await (0, ws_server_js_1.createWsTestServer)({
65
+ schema,
66
+ resolvedPreset,
67
+ pgSubscriber,
68
+ pgSubscriberKey,
69
+ buildPgSettings,
70
+ });
71
+ await (0, ws_helpers_js_1.delay)(300);
72
+ // 5. WS handle
73
+ const ws = {
74
+ serverUrl: wsServer.serverUrl,
75
+ createClient: (connectionParams) => wsServer.createClient(connectionParams),
76
+ nextEvent: (client, query, variables, timeoutMs) => (0, ws_helpers_js_1.nextEvent)(client, query, variables, timeoutMs),
77
+ collectEvents: (client, query, variables) => (0, ws_helpers_js_1.collectWsEvents)(client, query, variables),
78
+ dispose: () => wsServer.dispose(),
79
+ };
80
+ // 6. Notify helpers
81
+ const defaultSchema = schemas[0] ?? 'public';
82
+ const notifyClient = pg.client;
83
+ // 7. Teardown (idempotent)
84
+ let tornDown = false;
85
+ const teardown = async () => {
86
+ if (tornDown)
87
+ return;
88
+ tornDown = true;
89
+ // Order: clients -> server -> subscriber -> service -> pool -> db
90
+ await wsServer.dispose();
91
+ if (pgSubscriber && typeof pgSubscriber.release === 'function') {
92
+ await pgSubscriber.release();
93
+ }
94
+ if (pgService && typeof pgService.release === 'function') {
95
+ await pgService.release();
96
+ }
97
+ await dbTeardown();
98
+ };
99
+ return {
100
+ pg,
101
+ db,
102
+ schema,
103
+ resolvedPreset,
104
+ pgPool,
105
+ pgService,
106
+ pgSubscriber,
107
+ ws,
108
+ async notifyChange(table, operation, rowIds, tableSchema) {
109
+ await (0, notify_js_1.notifyChange)(notifyClient, tableSchema ?? defaultSchema, table, operation, rowIds);
110
+ },
111
+ async notifyInvalidate(table, tableSchema) {
112
+ await (0, notify_js_1.notifyInvalidate)(notifyClient, tableSchema ?? defaultSchema, table);
113
+ },
114
+ async notify(table, payload, tableSchema) {
115
+ await (0, notify_js_1.notify)(notifyClient, tableSchema ?? defaultSchema, table, payload);
116
+ },
117
+ teardown,
118
+ };
119
+ }
package/index.d.ts CHANGED
@@ -4,3 +4,8 @@ export { makeRealtimeSmartTagsPlugin } from './smart-tags.js';
4
4
  export { subscribe, waitForEvent, collectEvents, } from './subscribe.js';
5
5
  export type { SubscriptionEvent, SubscribeOptions } from './subscribe.js';
6
6
  export { notify, notifyChange, notifyInvalidate, buildPayload, buildInvalidatePayload, } from './notify.js';
7
+ export { nextEvent, collectWsEvents, delay } from './ws-helpers.js';
8
+ export { createWsTestServer } from './ws-server.js';
9
+ export type { WsTestServerInput, WsTestServer } from './ws-server.js';
10
+ export { getConnections } from './get-connections.js';
11
+ export type { GetConnectionsInput, GetConnectionsResult, WsHandle, } from './get-connections.js';
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildInvalidatePayload = exports.buildPayload = exports.notifyInvalidate = exports.notifyChange = exports.notify = exports.collectEvents = exports.waitForEvent = exports.subscribe = exports.makeRealtimeSmartTagsPlugin = exports.createRealtimeTestContext = void 0;
3
+ exports.getConnections = exports.createWsTestServer = exports.delay = exports.collectWsEvents = exports.nextEvent = exports.buildInvalidatePayload = exports.buildPayload = exports.notifyInvalidate = exports.notifyChange = exports.notify = exports.collectEvents = exports.waitForEvent = exports.subscribe = exports.makeRealtimeSmartTagsPlugin = exports.createRealtimeTestContext = void 0;
4
4
  var context_js_1 = require("./context.js");
5
5
  Object.defineProperty(exports, "createRealtimeTestContext", { enumerable: true, get: function () { return context_js_1.createRealtimeTestContext; } });
6
6
  var smart_tags_js_1 = require("./smart-tags.js");
@@ -15,3 +15,11 @@ Object.defineProperty(exports, "notifyChange", { enumerable: true, get: function
15
15
  Object.defineProperty(exports, "notifyInvalidate", { enumerable: true, get: function () { return notify_js_1.notifyInvalidate; } });
16
16
  Object.defineProperty(exports, "buildPayload", { enumerable: true, get: function () { return notify_js_1.buildPayload; } });
17
17
  Object.defineProperty(exports, "buildInvalidatePayload", { enumerable: true, get: function () { return notify_js_1.buildInvalidatePayload; } });
18
+ var ws_helpers_js_1 = require("./ws-helpers.js");
19
+ Object.defineProperty(exports, "nextEvent", { enumerable: true, get: function () { return ws_helpers_js_1.nextEvent; } });
20
+ Object.defineProperty(exports, "collectWsEvents", { enumerable: true, get: function () { return ws_helpers_js_1.collectWsEvents; } });
21
+ Object.defineProperty(exports, "delay", { enumerable: true, get: function () { return ws_helpers_js_1.delay; } });
22
+ var ws_server_js_1 = require("./ws-server.js");
23
+ Object.defineProperty(exports, "createWsTestServer", { enumerable: true, get: function () { return ws_server_js_1.createWsTestServer; } });
24
+ var get_connections_js_1 = require("./get-connections.js");
25
+ Object.defineProperty(exports, "getConnections", { enumerable: true, get: function () { return get_connections_js_1.getConnections; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-realtime-test",
3
- "version": "0.3.0",
3
+ "version": "0.6.0",
4
4
  "description": "Subscription testing utilities for graphile-realtime-subscriptions — smart tag injection, grafast.subscribe() helpers, and pg_notify simulation",
5
5
  "author": "Constructive <developers@constructive.io>",
6
6
  "homepage": "https://github.com/constructive-io/constructive",
@@ -39,7 +39,7 @@
39
39
  "url": "https://github.com/constructive-io/constructive/issues"
40
40
  },
41
41
  "dependencies": {
42
- "graphile-realtime-subscriptions": "^0.6.0",
42
+ "graphile-realtime-subscriptions": "^0.7.0",
43
43
  "graphile-test": "^4.15.0"
44
44
  },
45
45
  "peerDependencies": {
@@ -48,13 +48,20 @@
48
48
  "graphile-build-pg": "5.0.0",
49
49
  "graphile-config": "1.0.0",
50
50
  "graphql": "16.13.0",
51
+ "graphql-ws": "^6.0.8",
51
52
  "pg": "^8.20.0",
52
- "postgraphile": "5.0.0"
53
+ "postgraphile": "5.0.0",
54
+ "ws": "^8.20.0"
53
55
  },
54
56
  "devDependencies": {
57
+ "@types/express": "^5.0.6",
55
58
  "@types/pg": "^8.18.0",
59
+ "@types/ws": "^8.18.1",
60
+ "express": "^5.2.1",
61
+ "grafserv": "1.0.0",
56
62
  "makage": "^0.3.0",
57
- "pgsql-test": "^4.14.0"
63
+ "pgsql-test": "^4.14.0",
64
+ "postgraphile": "5.0.0"
58
65
  },
59
- "gitHead": "50dc1d69049c207b46c237eba1e1b1cb7a5f928f"
66
+ "gitHead": "79f9109e05713e24fbef527864e0df02634aa891"
60
67
  }
@@ -0,0 +1,19 @@
1
+ import type { Client as GqlWsClient } from 'graphql-ws';
2
+ /**
3
+ * Collect the next event from a graphql-ws WebSocket subscription.
4
+ *
5
+ * Returns a promise that resolves with the first `next` payload's `data`,
6
+ * or rejects on error / timeout. The subscription is automatically
7
+ * unsubscribed after the first event.
8
+ */
9
+ export declare function nextEvent<T = Record<string, unknown>>(client: GqlWsClient, query: string, variables?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
10
+ /**
11
+ * Subscribe and collect events into an array until `unsubscribe()` is called.
12
+ *
13
+ * Useful for assertions that need to inspect multiple events after the fact.
14
+ */
15
+ export declare function collectWsEvents<T = Record<string, unknown>>(client: GqlWsClient, query: string, variables?: Record<string, unknown>): {
16
+ events: T[];
17
+ unsubscribe: () => void;
18
+ };
19
+ export declare const delay: (ms: number) => Promise<void>;
package/ws-helpers.js ADDED
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.delay = void 0;
4
+ exports.nextEvent = nextEvent;
5
+ exports.collectWsEvents = collectWsEvents;
6
+ /**
7
+ * Collect the next event from a graphql-ws WebSocket subscription.
8
+ *
9
+ * Returns a promise that resolves with the first `next` payload's `data`,
10
+ * or rejects on error / timeout. The subscription is automatically
11
+ * unsubscribed after the first event.
12
+ */
13
+ function nextEvent(client, query, variables, timeoutMs = 10000) {
14
+ return new Promise((resolve, reject) => {
15
+ const timer = setTimeout(() => {
16
+ unsubscribe();
17
+ reject(new Error(`nextEvent timed out after ${timeoutMs}ms`));
18
+ }, timeoutMs);
19
+ const unsubscribe = client.subscribe({ query, variables }, {
20
+ next(value) {
21
+ clearTimeout(timer);
22
+ unsubscribe();
23
+ resolve(value.data);
24
+ },
25
+ error(err) {
26
+ clearTimeout(timer);
27
+ unsubscribe();
28
+ reject(Array.isArray(err) ? err[0] : err);
29
+ },
30
+ complete() {
31
+ clearTimeout(timer);
32
+ reject(new Error('Subscription completed without yielding a value'));
33
+ },
34
+ });
35
+ });
36
+ }
37
+ /**
38
+ * Subscribe and collect events into an array until `unsubscribe()` is called.
39
+ *
40
+ * Useful for assertions that need to inspect multiple events after the fact.
41
+ */
42
+ function collectWsEvents(client, query, variables) {
43
+ const events = [];
44
+ const unsubscribe = client.subscribe({ query, variables }, {
45
+ next(value) {
46
+ events.push(value.data);
47
+ },
48
+ error() { },
49
+ complete() { },
50
+ });
51
+ return { events, unsubscribe };
52
+ }
53
+ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
54
+ exports.delay = delay;
package/ws-server.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { type Server as HttpServer } from 'node:http';
2
+ import type { GraphQLSchema } from 'graphql';
3
+ import type { GraphileConfig } from 'graphile-config';
4
+ import { type Client as GqlWsClient } from 'graphql-ws';
5
+ import { WebSocketServer } from 'ws';
6
+ export interface WsTestServerInput {
7
+ schema: GraphQLSchema;
8
+ resolvedPreset: GraphileConfig.ResolvedPreset;
9
+ pgSubscriber: unknown;
10
+ pgSubscriberKey?: string;
11
+ buildPgSettings?: (connectionParams: Record<string, string>) => Record<string, string> | undefined;
12
+ }
13
+ export interface WsTestServer {
14
+ serverUrl: string;
15
+ httpServer: HttpServer;
16
+ wss: WebSocketServer;
17
+ createClient(connectionParams?: Record<string, unknown>): GqlWsClient;
18
+ dispose(): Promise<void>;
19
+ }
20
+ export declare function createWsTestServer(input: WsTestServerInput): Promise<WsTestServer>;
package/ws-server.js ADDED
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createWsTestServer = createWsTestServer;
4
+ const node_http_1 = require("node:http");
5
+ const grafast_1 = require("grafast");
6
+ const graphql_ws_1 = require("graphql-ws");
7
+ const ws_1 = require("graphql-ws/use/ws");
8
+ const ws_2 = require("ws");
9
+ async function createWsTestServer(input) {
10
+ const { schema, resolvedPreset, pgSubscriber, pgSubscriberKey = 'pgSubscriber', buildPgSettings, } = input;
11
+ const httpServer = (0, node_http_1.createServer)();
12
+ const wss = new ws_2.WebSocketServer({ server: httpServer, path: '/graphql' });
13
+ const serverCleanup = (0, ws_1.useServer)({
14
+ schema,
15
+ context: async (ctx) => ({
16
+ connectionParams: ctx.connectionParams,
17
+ }),
18
+ subscribe: async (args) => {
19
+ const params = args.contextValue?.connectionParams;
20
+ const contextValue = {
21
+ [pgSubscriberKey]: pgSubscriber,
22
+ ...(typeof args.contextValue === 'object' && args.contextValue !== null
23
+ ? args.contextValue
24
+ : {}),
25
+ };
26
+ if (buildPgSettings && params) {
27
+ const pgSettings = buildPgSettings(params);
28
+ if (pgSettings) {
29
+ contextValue['pgSettings'] = pgSettings;
30
+ }
31
+ }
32
+ const result = await (0, grafast_1.subscribe)({
33
+ schema: args.schema,
34
+ document: args.document,
35
+ variableValues: args.variableValues,
36
+ contextValue,
37
+ resolvedPreset,
38
+ });
39
+ return result;
40
+ },
41
+ }, wss);
42
+ await new Promise((resolve) => {
43
+ httpServer.listen(0, '127.0.0.1', () => resolve());
44
+ });
45
+ const addr = httpServer.address();
46
+ const serverUrl = `ws://127.0.0.1:${addr.port}/graphql`;
47
+ const clients = [];
48
+ return {
49
+ serverUrl,
50
+ httpServer,
51
+ wss,
52
+ createClient(connectionParams) {
53
+ const client = (0, graphql_ws_1.createClient)({
54
+ url: serverUrl,
55
+ webSocketImpl: ws_2.WebSocket,
56
+ retryAttempts: 0,
57
+ connectionParams,
58
+ });
59
+ clients.push(client);
60
+ return client;
61
+ },
62
+ async dispose() {
63
+ for (const client of clients) {
64
+ try {
65
+ await client.dispose();
66
+ }
67
+ catch { /* ignore */ }
68
+ }
69
+ await serverCleanup.dispose();
70
+ wss.close();
71
+ if (httpServer.listening) {
72
+ await new Promise((resolve) => httpServer.close(() => resolve()));
73
+ }
74
+ },
75
+ };
76
+ }