graphile-realtime-subscriptions 1.10.0 → 1.10.2

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,70 @@
1
+ /**
2
+ * The gate between PostgreSQL's NOTIFY stream and a GraphQL subscription.
3
+ *
4
+ * Everything that can decide "this event should not reach the client" lives
5
+ * here, upstream of grafast, because grafast's `listen` maps each event to a
6
+ * step and offers no way to suppress one. Deciding downstream forced the
7
+ * plugin to represent "dropped" as a null payload, which the field resolvers
8
+ * then had to coalesce (`p?.event ?? 'UNKNOWN'`) — so a filtered event still
9
+ * reached the client, carrying an invented event name. Filtering here means
10
+ * the stream only ever yields events that should exist, and every resolver
11
+ * downstream is total.
12
+ *
13
+ * The gate also owns parsing and the overflow throttle, so a payload the
14
+ * trigger and the plugin disagree about fails loudly instead of arriving as
15
+ * an `UNKNOWN`-shaped record.
16
+ */
17
+ import type { GrafastSubscriber } from 'grafast';
18
+ export interface ParsedPayload {
19
+ event: string;
20
+ /**
21
+ * Row ids being reported. Already narrowed to the subscription's `ids` when
22
+ * it supplied any, so no consumer needs to re-intersect. Empty for
23
+ * INVALIDATE.
24
+ */
25
+ rowIds: string[];
26
+ overflow: boolean;
27
+ }
28
+ export declare class MalformedNotifyPayloadError extends Error {
29
+ constructor(raw: string, reason: string);
30
+ }
31
+ /**
32
+ * Parse the NOTIFY payload from `emit_change`.
33
+ * Format: `"TG_OP:id1,id2,..."` or `"INVALIDATE"`.
34
+ *
35
+ * Throws on anything else: a payload this cannot read means the trigger and
36
+ * this plugin have diverged, and inventing an event name for it hides a
37
+ * deployment fault behind data the client will act on.
38
+ */
39
+ export declare function parseNotifyPayload(raw: string): ParsedPayload;
40
+ /**
41
+ * Per-subscription event rate tracker. Counts events in a sliding 1-second
42
+ * window.
43
+ */
44
+ export declare class EventThrottle {
45
+ private readonly threshold;
46
+ private windowStart;
47
+ private eventCount;
48
+ private overflowSent;
49
+ constructor(threshold: number);
50
+ /**
51
+ * Record an event and return whether it should be delivered.
52
+ * Returns 'deliver' for normal events, 'overflow' when the threshold
53
+ * is first exceeded, or 'drop' for subsequent events in the same window.
54
+ */
55
+ check(): 'deliver' | 'overflow' | 'drop';
56
+ }
57
+ export interface EventGateOptions {
58
+ /** Sparse-set subscription: deliver only events touching these row ids. */
59
+ ids?: readonly string[] | null;
60
+ /** Events per second before the gate collapses the stream to one INVALIDATE. */
61
+ threshold: number;
62
+ }
63
+ /**
64
+ * Wrap a `GrafastSubscriber` so its stream yields parsed, gated payloads.
65
+ *
66
+ * Built per subscription (the throttle and `ids` are per-subscriber state), so
67
+ * one noisy client can no longer throttle every other subscriber to the same
68
+ * table — which a single build-time throttle instance did.
69
+ */
70
+ export declare function createGatedSubscriber(inner: GrafastSubscriber<Record<string, string>>, { ids, threshold }: EventGateOptions): GrafastSubscriber<Record<string, ParsedPayload>>;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The gate between PostgreSQL's NOTIFY stream and a GraphQL subscription.
3
+ *
4
+ * Everything that can decide "this event should not reach the client" lives
5
+ * here, upstream of grafast, because grafast's `listen` maps each event to a
6
+ * step and offers no way to suppress one. Deciding downstream forced the
7
+ * plugin to represent "dropped" as a null payload, which the field resolvers
8
+ * then had to coalesce (`p?.event ?? 'UNKNOWN'`) — so a filtered event still
9
+ * reached the client, carrying an invented event name. Filtering here means
10
+ * the stream only ever yields events that should exist, and every resolver
11
+ * downstream is total.
12
+ *
13
+ * The gate also owns parsing and the overflow throttle, so a payload the
14
+ * trigger and the plugin disagree about fails loudly instead of arriving as
15
+ * an `UNKNOWN`-shaped record.
16
+ */
17
+ /** Operations `emit_change` is allowed to report. */
18
+ const KNOWN_OPERATIONS = new Set(['INSERT', 'UPDATE', 'DELETE']);
19
+ export class MalformedNotifyPayloadError extends Error {
20
+ constructor(raw, reason) {
21
+ super(`Malformed realtime NOTIFY payload ${JSON.stringify(raw)}: ${reason}. ` +
22
+ 'Expected "INVALIDATE" or "<INSERT|UPDATE|DELETE>:<uuid>[,<uuid>...]" ' +
23
+ 'as emitted by the emit_change trigger.');
24
+ this.name = 'MalformedNotifyPayloadError';
25
+ }
26
+ }
27
+ /**
28
+ * Parse the NOTIFY payload from `emit_change`.
29
+ * Format: `"TG_OP:id1,id2,..."` or `"INVALIDATE"`.
30
+ *
31
+ * Throws on anything else: a payload this cannot read means the trigger and
32
+ * this plugin have diverged, and inventing an event name for it hides a
33
+ * deployment fault behind data the client will act on.
34
+ */
35
+ export function parseNotifyPayload(raw) {
36
+ if (raw === 'INVALIDATE') {
37
+ return { event: 'INVALIDATE', rowIds: [], overflow: true };
38
+ }
39
+ const colonIdx = raw.indexOf(':');
40
+ if (colonIdx === -1) {
41
+ throw new MalformedNotifyPayloadError(raw, 'no ":" separating the operation from the row ids');
42
+ }
43
+ const event = raw.substring(0, colonIdx);
44
+ if (!KNOWN_OPERATIONS.has(event)) {
45
+ throw new MalformedNotifyPayloadError(raw, `unknown operation ${JSON.stringify(event)}`);
46
+ }
47
+ const idsPart = raw.substring(colonIdx + 1);
48
+ const rowIds = idsPart.length > 0 ? idsPart.split(',') : [];
49
+ if (rowIds.length === 0) {
50
+ throw new MalformedNotifyPayloadError(raw, `${event} carries no row ids`);
51
+ }
52
+ return { event, rowIds, overflow: false };
53
+ }
54
+ /**
55
+ * Per-subscription event rate tracker. Counts events in a sliding 1-second
56
+ * window.
57
+ */
58
+ export class EventThrottle {
59
+ threshold;
60
+ windowStart = 0;
61
+ eventCount = 0;
62
+ overflowSent = false;
63
+ constructor(threshold) {
64
+ this.threshold = threshold;
65
+ }
66
+ /**
67
+ * Record an event and return whether it should be delivered.
68
+ * Returns 'deliver' for normal events, 'overflow' when the threshold
69
+ * is first exceeded, or 'drop' for subsequent events in the same window.
70
+ */
71
+ check() {
72
+ const now = Date.now();
73
+ if (now - this.windowStart >= 1000) {
74
+ this.windowStart = now;
75
+ this.eventCount = 0;
76
+ this.overflowSent = false;
77
+ }
78
+ this.eventCount++;
79
+ if (this.eventCount <= this.threshold) {
80
+ return 'deliver';
81
+ }
82
+ if (!this.overflowSent) {
83
+ this.overflowSent = true;
84
+ return 'overflow';
85
+ }
86
+ return 'drop';
87
+ }
88
+ }
89
+ /**
90
+ * Apply the gate to one raw NOTIFY payload.
91
+ *
92
+ * Returns the payload to deliver, or `null` meaning *do not emit* — a decision
93
+ * only this layer is allowed to make, because only this layer can act on it.
94
+ */
95
+ function gate(raw, ids, throttle) {
96
+ const parsed = parseNotifyPayload(raw);
97
+ // An INVALIDATE is the overflow signal itself; throttling it would drop the
98
+ // one event telling the client to refetch.
99
+ if (parsed.overflow)
100
+ return parsed;
101
+ const action = throttle.check();
102
+ if (action === 'drop')
103
+ return null;
104
+ if (action === 'overflow') {
105
+ return { event: 'INVALIDATE', rowIds: [], overflow: true };
106
+ }
107
+ if (!ids || ids.length === 0)
108
+ return parsed;
109
+ const matched = parsed.rowIds.filter(rowId => ids.includes(rowId));
110
+ if (matched.length === 0)
111
+ return null;
112
+ return { ...parsed, rowIds: matched };
113
+ }
114
+ /**
115
+ * Wrap a `GrafastSubscriber` so its stream yields parsed, gated payloads.
116
+ *
117
+ * Built per subscription (the throttle and `ids` are per-subscriber state), so
118
+ * one noisy client can no longer throttle every other subscriber to the same
119
+ * table — which a single build-time throttle instance did.
120
+ */
121
+ export function createGatedSubscriber(inner, { ids, threshold }) {
122
+ const subscribedIds = ids && ids.length > 0 ? ids : null;
123
+ const throttle = new EventThrottle(threshold);
124
+ return {
125
+ async *subscribe(topic) {
126
+ const source = await inner.subscribe(topic);
127
+ for await (const raw of source) {
128
+ const payload = gate(String(raw), subscribedIds, throttle);
129
+ if (payload !== null)
130
+ yield payload;
131
+ }
132
+ }
133
+ // Deliberately no release(): `inner` is the shared pgSubscriber from the
134
+ // request context and is not ours to tear down. Ending iteration returns
135
+ // the underlying iterator, which is the whole of our cleanup.
136
+ };
137
+ }
package/esm/plugin.d.ts CHANGED
@@ -17,8 +17,10 @@
17
17
  * 1. A row is inserted/updated/deleted
18
18
  * 2. The emit_change trigger fires pg_notify with TG_OP:row_ids or INVALIDATE
19
19
  * 3. PostGraphile's pgSubscriber receives the NOTIFY
20
- * 4. The plugin parses the payload and fetches the specific changed row(s)
21
- * 5. The client receives { event, row, rowId, overflow }
20
+ * 4. The gate (see event-gate.ts) parses, throttles and filters it — a
21
+ * payload this subscription should not see never becomes an event
22
+ * 5. The plugin fetches the changed row(s)
23
+ * 6. The client receives { event, row, rowId, overflow }
22
24
  *
23
25
  * Cursor tracking (at-least-once delivery):
24
26
  * The CursorTracker class provides a complementary polling-based delivery
@@ -31,7 +33,7 @@
31
33
  *
32
34
  * Overflow protection:
33
35
  * - Database-side: statements affecting > 50 rows send INVALIDATE
34
- * - Plugin-side: per-subscriber throttle (default 50 events/second/table)
36
+ * - Plugin-side: per-subscription throttle (default 50 events/second/table)
35
37
  * drops individual events and sends a single INVALIDATE when exceeded
36
38
  *
37
39
  * Security / RLS enforcement:
@@ -49,36 +51,11 @@ import type { GraphileConfig } from 'graphile-config';
49
51
  import type { RealtimeSubscriptionsPluginOptions } from './types';
50
52
  /** Default overflow threshold: events per second per table per subscriber */
51
53
  declare const DEFAULT_OVERFLOW_THRESHOLD = 50;
52
- interface ParsedPayload {
53
- event: string;
54
- rowIds: string[];
55
- overflow: boolean;
56
- }
57
- /**
58
- * Parse the NOTIFY payload from emit_change.
59
- * Format: "TG_OP:id1,id2,..." or "INVALIDATE"
60
- */
61
- declare function parseNotifyPayload(raw: string): ParsedPayload;
62
- /**
63
- * Per-subscriber, per-table event rate tracker.
64
- * Counts events in a sliding 1-second window.
65
- */
66
- declare class EventThrottle {
67
- private readonly threshold;
68
- private windowStart;
69
- private eventCount;
70
- private overflowSent;
71
- constructor(threshold: number);
72
- /**
73
- * Record an event and return whether it should be delivered.
74
- * Returns 'deliver' for normal events, 'overflow' when the threshold
75
- * is first exceeded, or 'drop' for subsequent events in the same window.
76
- */
77
- check(): 'deliver' | 'overflow' | 'drop';
78
- }
79
54
  export declare function createRealtimeSubscriptionsPlugin(options?: RealtimeSubscriptionsPluginOptions): GraphileConfig.Plugin;
80
55
  export { createRealtimeSubscriptionsPlugin as RealtimeSubscriptionsPlugin };
81
56
  export { CursorTracker } from './cursor-tracker';
57
+ export type { ParsedPayload } from './event-gate';
58
+ export { createGatedSubscriber, EventThrottle, MalformedNotifyPayloadError, parseNotifyPayload, } from './event-gate';
82
59
  export { RealtimeManager } from './realtime-manager';
83
60
  export type { ChangeLogEntry, CursorTrackerOptions, Queryable, RealtimeManagerOptions } from './types';
84
- export { DEFAULT_OVERFLOW_THRESHOLD, EventThrottle, parseNotifyPayload };
61
+ export { DEFAULT_OVERFLOW_THRESHOLD };
package/esm/plugin.js CHANGED
@@ -17,8 +17,10 @@
17
17
  * 1. A row is inserted/updated/deleted
18
18
  * 2. The emit_change trigger fires pg_notify with TG_OP:row_ids or INVALIDATE
19
19
  * 3. PostGraphile's pgSubscriber receives the NOTIFY
20
- * 4. The plugin parses the payload and fetches the specific changed row(s)
21
- * 5. The client receives { event, row, rowId, overflow }
20
+ * 4. The gate (see event-gate.ts) parses, throttles and filters it — a
21
+ * payload this subscription should not see never becomes an event
22
+ * 5. The plugin fetches the changed row(s)
23
+ * 6. The client receives { event, row, rowId, overflow }
22
24
  *
23
25
  * Cursor tracking (at-least-once delivery):
24
26
  * The CursorTracker class provides a complementary polling-based delivery
@@ -31,7 +33,7 @@
31
33
  *
32
34
  * Overflow protection:
33
35
  * - Database-side: statements affecting > 50 rows send INVALIDATE
34
- * - Plugin-side: per-subscriber throttle (default 50 events/second/table)
36
+ * - Plugin-side: per-subscription throttle (default 50 events/second/table)
35
37
  * drops individual events and sends a single INVALIDATE when exceeded
36
38
  *
37
39
  * Security / RLS enforcement:
@@ -48,61 +50,10 @@
48
50
  import { Logger } from '@pgpmjs/logger';
49
51
  import { constant, context as grafastContext, lambda, listen, object } from 'grafast';
50
52
  import { extendSchema } from 'graphile-utils';
53
+ import { createGatedSubscriber } from './event-gate';
51
54
  const log = new Logger('graphile-realtime-subscriptions');
52
55
  /** Default overflow threshold: events per second per table per subscriber */
53
56
  const DEFAULT_OVERFLOW_THRESHOLD = 50;
54
- /**
55
- * Parse the NOTIFY payload from emit_change.
56
- * Format: "TG_OP:id1,id2,..." or "INVALIDATE"
57
- */
58
- function parseNotifyPayload(raw) {
59
- if (raw === 'INVALIDATE') {
60
- return { event: 'INVALIDATE', rowIds: [], overflow: true };
61
- }
62
- const colonIdx = raw.indexOf(':');
63
- if (colonIdx === -1) {
64
- return { event: raw || 'UNKNOWN', rowIds: [], overflow: false };
65
- }
66
- const event = raw.substring(0, colonIdx);
67
- const idsPart = raw.substring(colonIdx + 1);
68
- const rowIds = idsPart.length > 0 ? idsPart.split(',') : [];
69
- return { event, rowIds, overflow: false };
70
- }
71
- /**
72
- * Per-subscriber, per-table event rate tracker.
73
- * Counts events in a sliding 1-second window.
74
- */
75
- class EventThrottle {
76
- threshold;
77
- windowStart = 0;
78
- eventCount = 0;
79
- overflowSent = false;
80
- constructor(threshold) {
81
- this.threshold = threshold;
82
- }
83
- /**
84
- * Record an event and return whether it should be delivered.
85
- * Returns 'deliver' for normal events, 'overflow' when the threshold
86
- * is first exceeded, or 'drop' for subsequent events in the same window.
87
- */
88
- check() {
89
- const now = Date.now();
90
- if (now - this.windowStart >= 1000) {
91
- this.windowStart = now;
92
- this.eventCount = 0;
93
- this.overflowSent = false;
94
- }
95
- this.eventCount++;
96
- if (this.eventCount <= this.threshold) {
97
- return 'deliver';
98
- }
99
- if (!this.overflowSent) {
100
- this.overflowSent = true;
101
- return 'overflow';
102
- }
103
- return 'drop';
104
- }
105
- }
106
57
  function discoverRealtimeTables(build) {
107
58
  const { pgRegistry } = build.input;
108
59
  const resources = pgRegistry.pgResources;
@@ -155,44 +106,44 @@ function buildTypeDefs(tables) {
155
106
  .join('\n\n');
156
107
  return `extend type Subscription {\n${subscriptionFields}\n}\n\n${payloadTypes}`;
157
108
  }
109
+ /**
110
+ * The gate never emits a null payload, so one here means the plan graph was
111
+ * rewired wrongly — fail rather than invent an event for the client.
112
+ */
113
+ function requirePayload(payload) {
114
+ if (payload === null || payload === undefined) {
115
+ throw new Error('Realtime subscription payload is missing: the gated subscriber only ever ' +
116
+ 'yields parsed payloads, so this event bypassed createGatedSubscriber.');
117
+ }
118
+ return payload;
119
+ }
120
+ /**
121
+ * The row this event reports. `rowIds` is already narrowed to the
122
+ * subscription's `ids`, so the first entry is the one to surface; INVALIDATE
123
+ * carries none, which is a genuine absence rather than a suppressed error.
124
+ */
125
+ function reportedRowId(payload) {
126
+ if (payload.overflow)
127
+ return null;
128
+ return payload.rowIds[0] ?? null;
129
+ }
158
130
  function buildPlans(tables, overflowThreshold) {
159
131
  const subscriptionPlans = {};
160
132
  const allPlans = {};
161
133
  for (const { resource, fieldName, payloadTypeName, rowFieldName, notifyChannel } of tables) {
162
- const throttle = new EventThrottle(overflowThreshold);
163
134
  subscriptionPlans[fieldName] = {
164
135
  subscribePlan(_$root, args) {
165
136
  const $pgSubscriber = grafastContext().get('pgSubscriber');
166
137
  const $topic = constant(notifyChannel);
167
138
  const $ids = args.getRaw('ids');
168
- return listen($pgSubscriber, $topic, ($payload) => {
169
- const $parsed = lambda([$payload, $ids], (pair) => {
170
- const [raw, subscribedIds] = pair;
171
- const parsed = parseNotifyPayload(String(raw));
172
- const action = parsed.overflow ? 'deliver' : throttle.check();
173
- if (action === 'drop') {
174
- return null;
175
- }
176
- if (action === 'overflow') {
177
- return {
178
- event: 'INVALIDATE',
179
- rowIds: [],
180
- overflow: true,
181
- };
182
- }
183
- // Sparse set filtering: only deliver events for subscribed row IDs
184
- if (subscribedIds && subscribedIds.length > 0) {
185
- const hasMatch = parsed.rowIds.some((rid) => subscribedIds.includes(rid));
186
- if (!hasMatch)
187
- return null;
188
- }
189
- return parsed;
190
- });
191
- return object({
192
- parsed: $parsed,
193
- subscribedIds: $ids,
194
- });
139
+ // Parsing, throttling and sparse-set filtering all happen in the gate,
140
+ // built once per subscription, so the stream below yields only events
141
+ // that should reach this client — every step after it is total.
142
+ const $subscriber = lambda([$pgSubscriber, $ids], (pair) => {
143
+ const [pgSubscriber, ids] = pair;
144
+ return createGatedSubscriber(pgSubscriber, { ids, threshold: overflowThreshold });
195
145
  });
146
+ return listen($subscriber, $topic, ($payload) => object({ parsed: $payload }));
196
147
  },
197
148
  plan($event) {
198
149
  return $event;
@@ -200,41 +151,16 @@ function buildPlans(tables, overflowThreshold) {
200
151
  };
201
152
  allPlans[payloadTypeName] = {
202
153
  event($parent) {
203
- const $parsed = $parent.get('parsed');
204
- return lambda($parsed, (p) => p?.event ?? 'UNKNOWN');
154
+ return lambda($parent.get('parsed'), (p) => requirePayload(p).event);
205
155
  },
206
156
  rowId($parent) {
207
- const $parsed = $parent.get('parsed');
208
- const $subscribedIds = $parent.get('subscribedIds');
209
- return lambda([$parsed, $subscribedIds], (pair) => {
210
- const [p, subscribedIds] = pair;
211
- if (!p || p.overflow || p.rowIds.length === 0)
212
- return null;
213
- // When ids are provided, return the first matching row ID
214
- if (subscribedIds && subscribedIds.length > 0) {
215
- return p.rowIds.find((rid) => subscribedIds.includes(rid)) ?? null;
216
- }
217
- return p.rowIds[0];
218
- });
157
+ return lambda($parent.get('parsed'), (p) => reportedRowId(requirePayload(p)));
219
158
  },
220
159
  overflow($parent) {
221
- const $parsed = $parent.get('parsed');
222
- return lambda($parsed, (p) => p?.overflow ?? false);
160
+ return lambda($parent.get('parsed'), (p) => requirePayload(p).overflow);
223
161
  },
224
162
  [rowFieldName]($parent) {
225
- const $parsed = $parent.get('parsed');
226
- const $subscribedIds = $parent.get('subscribedIds');
227
- const $rowId = lambda([$parsed, $subscribedIds], (tuple) => {
228
- const [p, subscribedIds] = tuple;
229
- if (!p || p.overflow || p.rowIds.length === 0)
230
- return null;
231
- // When ids are provided, return first matching row ID
232
- if (subscribedIds && subscribedIds.length > 0) {
233
- return p.rowIds.find((rid) => subscribedIds.includes(rid)) ?? null;
234
- }
235
- // Full collection mode: return first row ID
236
- return p.rowIds[0];
237
- });
163
+ const $rowId = lambda($parent.get('parsed'), (p) => reportedRowId(requirePayload(p)));
238
164
  return resource.get({ id: $rowId });
239
165
  },
240
166
  };
@@ -260,6 +186,6 @@ export function createRealtimeSubscriptionsPlugin(options = {}) {
260
186
  export { createRealtimeSubscriptionsPlugin as RealtimeSubscriptionsPlugin };
261
187
  // Re-export CursorTracker and RealtimeManager for convenience
262
188
  export { CursorTracker } from './cursor-tracker';
189
+ export { createGatedSubscriber, EventThrottle, MalformedNotifyPayloadError, parseNotifyPayload, } from './event-gate';
263
190
  export { RealtimeManager } from './realtime-manager';
264
- // Exported for testing
265
- export { DEFAULT_OVERFLOW_THRESHOLD, EventThrottle, parseNotifyPayload };
191
+ export { DEFAULT_OVERFLOW_THRESHOLD };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The gate between PostgreSQL's NOTIFY stream and a GraphQL subscription.
3
+ *
4
+ * Everything that can decide "this event should not reach the client" lives
5
+ * here, upstream of grafast, because grafast's `listen` maps each event to a
6
+ * step and offers no way to suppress one. Deciding downstream forced the
7
+ * plugin to represent "dropped" as a null payload, which the field resolvers
8
+ * then had to coalesce (`p?.event ?? 'UNKNOWN'`) — so a filtered event still
9
+ * reached the client, carrying an invented event name. Filtering here means
10
+ * the stream only ever yields events that should exist, and every resolver
11
+ * downstream is total.
12
+ *
13
+ * The gate also owns parsing and the overflow throttle, so a payload the
14
+ * trigger and the plugin disagree about fails loudly instead of arriving as
15
+ * an `UNKNOWN`-shaped record.
16
+ */
17
+ import type { GrafastSubscriber } from 'grafast';
18
+ export interface ParsedPayload {
19
+ event: string;
20
+ /**
21
+ * Row ids being reported. Already narrowed to the subscription's `ids` when
22
+ * it supplied any, so no consumer needs to re-intersect. Empty for
23
+ * INVALIDATE.
24
+ */
25
+ rowIds: string[];
26
+ overflow: boolean;
27
+ }
28
+ export declare class MalformedNotifyPayloadError extends Error {
29
+ constructor(raw: string, reason: string);
30
+ }
31
+ /**
32
+ * Parse the NOTIFY payload from `emit_change`.
33
+ * Format: `"TG_OP:id1,id2,..."` or `"INVALIDATE"`.
34
+ *
35
+ * Throws on anything else: a payload this cannot read means the trigger and
36
+ * this plugin have diverged, and inventing an event name for it hides a
37
+ * deployment fault behind data the client will act on.
38
+ */
39
+ export declare function parseNotifyPayload(raw: string): ParsedPayload;
40
+ /**
41
+ * Per-subscription event rate tracker. Counts events in a sliding 1-second
42
+ * window.
43
+ */
44
+ export declare class EventThrottle {
45
+ private readonly threshold;
46
+ private windowStart;
47
+ private eventCount;
48
+ private overflowSent;
49
+ constructor(threshold: number);
50
+ /**
51
+ * Record an event and return whether it should be delivered.
52
+ * Returns 'deliver' for normal events, 'overflow' when the threshold
53
+ * is first exceeded, or 'drop' for subsequent events in the same window.
54
+ */
55
+ check(): 'deliver' | 'overflow' | 'drop';
56
+ }
57
+ export interface EventGateOptions {
58
+ /** Sparse-set subscription: deliver only events touching these row ids. */
59
+ ids?: readonly string[] | null;
60
+ /** Events per second before the gate collapses the stream to one INVALIDATE. */
61
+ threshold: number;
62
+ }
63
+ /**
64
+ * Wrap a `GrafastSubscriber` so its stream yields parsed, gated payloads.
65
+ *
66
+ * Built per subscription (the throttle and `ids` are per-subscriber state), so
67
+ * one noisy client can no longer throttle every other subscriber to the same
68
+ * table — which a single build-time throttle instance did.
69
+ */
70
+ export declare function createGatedSubscriber(inner: GrafastSubscriber<Record<string, string>>, { ids, threshold }: EventGateOptions): GrafastSubscriber<Record<string, ParsedPayload>>;
package/event-gate.js ADDED
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ /**
3
+ * The gate between PostgreSQL's NOTIFY stream and a GraphQL subscription.
4
+ *
5
+ * Everything that can decide "this event should not reach the client" lives
6
+ * here, upstream of grafast, because grafast's `listen` maps each event to a
7
+ * step and offers no way to suppress one. Deciding downstream forced the
8
+ * plugin to represent "dropped" as a null payload, which the field resolvers
9
+ * then had to coalesce (`p?.event ?? 'UNKNOWN'`) — so a filtered event still
10
+ * reached the client, carrying an invented event name. Filtering here means
11
+ * the stream only ever yields events that should exist, and every resolver
12
+ * downstream is total.
13
+ *
14
+ * The gate also owns parsing and the overflow throttle, so a payload the
15
+ * trigger and the plugin disagree about fails loudly instead of arriving as
16
+ * an `UNKNOWN`-shaped record.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.EventThrottle = exports.MalformedNotifyPayloadError = void 0;
20
+ exports.parseNotifyPayload = parseNotifyPayload;
21
+ exports.createGatedSubscriber = createGatedSubscriber;
22
+ /** Operations `emit_change` is allowed to report. */
23
+ const KNOWN_OPERATIONS = new Set(['INSERT', 'UPDATE', 'DELETE']);
24
+ class MalformedNotifyPayloadError extends Error {
25
+ constructor(raw, reason) {
26
+ super(`Malformed realtime NOTIFY payload ${JSON.stringify(raw)}: ${reason}. ` +
27
+ 'Expected "INVALIDATE" or "<INSERT|UPDATE|DELETE>:<uuid>[,<uuid>...]" ' +
28
+ 'as emitted by the emit_change trigger.');
29
+ this.name = 'MalformedNotifyPayloadError';
30
+ }
31
+ }
32
+ exports.MalformedNotifyPayloadError = MalformedNotifyPayloadError;
33
+ /**
34
+ * Parse the NOTIFY payload from `emit_change`.
35
+ * Format: `"TG_OP:id1,id2,..."` or `"INVALIDATE"`.
36
+ *
37
+ * Throws on anything else: a payload this cannot read means the trigger and
38
+ * this plugin have diverged, and inventing an event name for it hides a
39
+ * deployment fault behind data the client will act on.
40
+ */
41
+ function parseNotifyPayload(raw) {
42
+ if (raw === 'INVALIDATE') {
43
+ return { event: 'INVALIDATE', rowIds: [], overflow: true };
44
+ }
45
+ const colonIdx = raw.indexOf(':');
46
+ if (colonIdx === -1) {
47
+ throw new MalformedNotifyPayloadError(raw, 'no ":" separating the operation from the row ids');
48
+ }
49
+ const event = raw.substring(0, colonIdx);
50
+ if (!KNOWN_OPERATIONS.has(event)) {
51
+ throw new MalformedNotifyPayloadError(raw, `unknown operation ${JSON.stringify(event)}`);
52
+ }
53
+ const idsPart = raw.substring(colonIdx + 1);
54
+ const rowIds = idsPart.length > 0 ? idsPart.split(',') : [];
55
+ if (rowIds.length === 0) {
56
+ throw new MalformedNotifyPayloadError(raw, `${event} carries no row ids`);
57
+ }
58
+ return { event, rowIds, overflow: false };
59
+ }
60
+ /**
61
+ * Per-subscription event rate tracker. Counts events in a sliding 1-second
62
+ * window.
63
+ */
64
+ class EventThrottle {
65
+ threshold;
66
+ windowStart = 0;
67
+ eventCount = 0;
68
+ overflowSent = false;
69
+ constructor(threshold) {
70
+ this.threshold = threshold;
71
+ }
72
+ /**
73
+ * Record an event and return whether it should be delivered.
74
+ * Returns 'deliver' for normal events, 'overflow' when the threshold
75
+ * is first exceeded, or 'drop' for subsequent events in the same window.
76
+ */
77
+ check() {
78
+ const now = Date.now();
79
+ if (now - this.windowStart >= 1000) {
80
+ this.windowStart = now;
81
+ this.eventCount = 0;
82
+ this.overflowSent = false;
83
+ }
84
+ this.eventCount++;
85
+ if (this.eventCount <= this.threshold) {
86
+ return 'deliver';
87
+ }
88
+ if (!this.overflowSent) {
89
+ this.overflowSent = true;
90
+ return 'overflow';
91
+ }
92
+ return 'drop';
93
+ }
94
+ }
95
+ exports.EventThrottle = EventThrottle;
96
+ /**
97
+ * Apply the gate to one raw NOTIFY payload.
98
+ *
99
+ * Returns the payload to deliver, or `null` meaning *do not emit* — a decision
100
+ * only this layer is allowed to make, because only this layer can act on it.
101
+ */
102
+ function gate(raw, ids, throttle) {
103
+ const parsed = parseNotifyPayload(raw);
104
+ // An INVALIDATE is the overflow signal itself; throttling it would drop the
105
+ // one event telling the client to refetch.
106
+ if (parsed.overflow)
107
+ return parsed;
108
+ const action = throttle.check();
109
+ if (action === 'drop')
110
+ return null;
111
+ if (action === 'overflow') {
112
+ return { event: 'INVALIDATE', rowIds: [], overflow: true };
113
+ }
114
+ if (!ids || ids.length === 0)
115
+ return parsed;
116
+ const matched = parsed.rowIds.filter(rowId => ids.includes(rowId));
117
+ if (matched.length === 0)
118
+ return null;
119
+ return { ...parsed, rowIds: matched };
120
+ }
121
+ /**
122
+ * Wrap a `GrafastSubscriber` so its stream yields parsed, gated payloads.
123
+ *
124
+ * Built per subscription (the throttle and `ids` are per-subscriber state), so
125
+ * one noisy client can no longer throttle every other subscriber to the same
126
+ * table — which a single build-time throttle instance did.
127
+ */
128
+ function createGatedSubscriber(inner, { ids, threshold }) {
129
+ const subscribedIds = ids && ids.length > 0 ? ids : null;
130
+ const throttle = new EventThrottle(threshold);
131
+ return {
132
+ async *subscribe(topic) {
133
+ const source = await inner.subscribe(topic);
134
+ for await (const raw of source) {
135
+ const payload = gate(String(raw), subscribedIds, throttle);
136
+ if (payload !== null)
137
+ yield payload;
138
+ }
139
+ }
140
+ // Deliberately no release(): `inner` is the shared pgSubscriber from the
141
+ // request context and is not ours to tear down. Ending iteration returns
142
+ // the underlying iterator, which is the whole of our cleanup.
143
+ };
144
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-realtime-subscriptions",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
4
4
  "description": "Realtime subscription plugin for PostGraphile v5 — per-table GraphQL subscriptions via LISTEN/NOTIFY",
5
5
  "author": "Constructive <developers@constructive.io>",
6
6
  "homepage": "https://github.com/constructive-io/constructive",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@pgpmjs/logger": "^2.24.0",
44
- "@pgsql/quotes": "^18.2.0"
44
+ "@pgsql/quotes": "^18.2.1"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "grafast": "1.0.2",
@@ -56,5 +56,5 @@
56
56
  "@types/node": "^22.19.11",
57
57
  "makage": "^0.3.0"
58
58
  },
59
- "gitHead": "d508453a32a2f1d70ff6916871046615226e6f33"
59
+ "gitHead": "43121004a8bedd2c5fcf7bf33208079d55bad01f"
60
60
  }
package/plugin.d.ts CHANGED
@@ -17,8 +17,10 @@
17
17
  * 1. A row is inserted/updated/deleted
18
18
  * 2. The emit_change trigger fires pg_notify with TG_OP:row_ids or INVALIDATE
19
19
  * 3. PostGraphile's pgSubscriber receives the NOTIFY
20
- * 4. The plugin parses the payload and fetches the specific changed row(s)
21
- * 5. The client receives { event, row, rowId, overflow }
20
+ * 4. The gate (see event-gate.ts) parses, throttles and filters it — a
21
+ * payload this subscription should not see never becomes an event
22
+ * 5. The plugin fetches the changed row(s)
23
+ * 6. The client receives { event, row, rowId, overflow }
22
24
  *
23
25
  * Cursor tracking (at-least-once delivery):
24
26
  * The CursorTracker class provides a complementary polling-based delivery
@@ -31,7 +33,7 @@
31
33
  *
32
34
  * Overflow protection:
33
35
  * - Database-side: statements affecting > 50 rows send INVALIDATE
34
- * - Plugin-side: per-subscriber throttle (default 50 events/second/table)
36
+ * - Plugin-side: per-subscription throttle (default 50 events/second/table)
35
37
  * drops individual events and sends a single INVALIDATE when exceeded
36
38
  *
37
39
  * Security / RLS enforcement:
@@ -49,36 +51,11 @@ import type { GraphileConfig } from 'graphile-config';
49
51
  import type { RealtimeSubscriptionsPluginOptions } from './types';
50
52
  /** Default overflow threshold: events per second per table per subscriber */
51
53
  declare const DEFAULT_OVERFLOW_THRESHOLD = 50;
52
- interface ParsedPayload {
53
- event: string;
54
- rowIds: string[];
55
- overflow: boolean;
56
- }
57
- /**
58
- * Parse the NOTIFY payload from emit_change.
59
- * Format: "TG_OP:id1,id2,..." or "INVALIDATE"
60
- */
61
- declare function parseNotifyPayload(raw: string): ParsedPayload;
62
- /**
63
- * Per-subscriber, per-table event rate tracker.
64
- * Counts events in a sliding 1-second window.
65
- */
66
- declare class EventThrottle {
67
- private readonly threshold;
68
- private windowStart;
69
- private eventCount;
70
- private overflowSent;
71
- constructor(threshold: number);
72
- /**
73
- * Record an event and return whether it should be delivered.
74
- * Returns 'deliver' for normal events, 'overflow' when the threshold
75
- * is first exceeded, or 'drop' for subsequent events in the same window.
76
- */
77
- check(): 'deliver' | 'overflow' | 'drop';
78
- }
79
54
  export declare function createRealtimeSubscriptionsPlugin(options?: RealtimeSubscriptionsPluginOptions): GraphileConfig.Plugin;
80
55
  export { createRealtimeSubscriptionsPlugin as RealtimeSubscriptionsPlugin };
81
56
  export { CursorTracker } from './cursor-tracker';
57
+ export type { ParsedPayload } from './event-gate';
58
+ export { createGatedSubscriber, EventThrottle, MalformedNotifyPayloadError, parseNotifyPayload, } from './event-gate';
82
59
  export { RealtimeManager } from './realtime-manager';
83
60
  export type { ChangeLogEntry, CursorTrackerOptions, Queryable, RealtimeManagerOptions } from './types';
84
- export { DEFAULT_OVERFLOW_THRESHOLD, EventThrottle, parseNotifyPayload };
61
+ export { DEFAULT_OVERFLOW_THRESHOLD };
package/plugin.js CHANGED
@@ -18,8 +18,10 @@
18
18
  * 1. A row is inserted/updated/deleted
19
19
  * 2. The emit_change trigger fires pg_notify with TG_OP:row_ids or INVALIDATE
20
20
  * 3. PostGraphile's pgSubscriber receives the NOTIFY
21
- * 4. The plugin parses the payload and fetches the specific changed row(s)
22
- * 5. The client receives { event, row, rowId, overflow }
21
+ * 4. The gate (see event-gate.ts) parses, throttles and filters it — a
22
+ * payload this subscription should not see never becomes an event
23
+ * 5. The plugin fetches the changed row(s)
24
+ * 6. The client receives { event, row, rowId, overflow }
23
25
  *
24
26
  * Cursor tracking (at-least-once delivery):
25
27
  * The CursorTracker class provides a complementary polling-based delivery
@@ -32,7 +34,7 @@
32
34
  *
33
35
  * Overflow protection:
34
36
  * - Database-side: statements affecting > 50 rows send INVALIDATE
35
- * - Plugin-side: per-subscriber throttle (default 50 events/second/table)
37
+ * - Plugin-side: per-subscription throttle (default 50 events/second/table)
36
38
  * drops individual events and sends a single INVALIDATE when exceeded
37
39
  *
38
40
  * Security / RLS enforcement:
@@ -47,70 +49,17 @@
47
49
  * preventing cross-tenant event leaks.
48
50
  */
49
51
  Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.EventThrottle = exports.DEFAULT_OVERFLOW_THRESHOLD = exports.RealtimeManager = exports.CursorTracker = void 0;
52
+ exports.DEFAULT_OVERFLOW_THRESHOLD = exports.RealtimeManager = exports.parseNotifyPayload = exports.MalformedNotifyPayloadError = exports.EventThrottle = exports.createGatedSubscriber = exports.CursorTracker = void 0;
51
53
  exports.createRealtimeSubscriptionsPlugin = createRealtimeSubscriptionsPlugin;
52
54
  exports.RealtimeSubscriptionsPlugin = createRealtimeSubscriptionsPlugin;
53
- exports.parseNotifyPayload = parseNotifyPayload;
54
55
  const logger_1 = require("@pgpmjs/logger");
55
56
  const grafast_1 = require("grafast");
56
57
  const graphile_utils_1 = require("graphile-utils");
58
+ const event_gate_1 = require("./event-gate");
57
59
  const log = new logger_1.Logger('graphile-realtime-subscriptions');
58
60
  /** Default overflow threshold: events per second per table per subscriber */
59
61
  const DEFAULT_OVERFLOW_THRESHOLD = 50;
60
62
  exports.DEFAULT_OVERFLOW_THRESHOLD = DEFAULT_OVERFLOW_THRESHOLD;
61
- /**
62
- * Parse the NOTIFY payload from emit_change.
63
- * Format: "TG_OP:id1,id2,..." or "INVALIDATE"
64
- */
65
- function parseNotifyPayload(raw) {
66
- if (raw === 'INVALIDATE') {
67
- return { event: 'INVALIDATE', rowIds: [], overflow: true };
68
- }
69
- const colonIdx = raw.indexOf(':');
70
- if (colonIdx === -1) {
71
- return { event: raw || 'UNKNOWN', rowIds: [], overflow: false };
72
- }
73
- const event = raw.substring(0, colonIdx);
74
- const idsPart = raw.substring(colonIdx + 1);
75
- const rowIds = idsPart.length > 0 ? idsPart.split(',') : [];
76
- return { event, rowIds, overflow: false };
77
- }
78
- /**
79
- * Per-subscriber, per-table event rate tracker.
80
- * Counts events in a sliding 1-second window.
81
- */
82
- class EventThrottle {
83
- threshold;
84
- windowStart = 0;
85
- eventCount = 0;
86
- overflowSent = false;
87
- constructor(threshold) {
88
- this.threshold = threshold;
89
- }
90
- /**
91
- * Record an event and return whether it should be delivered.
92
- * Returns 'deliver' for normal events, 'overflow' when the threshold
93
- * is first exceeded, or 'drop' for subsequent events in the same window.
94
- */
95
- check() {
96
- const now = Date.now();
97
- if (now - this.windowStart >= 1000) {
98
- this.windowStart = now;
99
- this.eventCount = 0;
100
- this.overflowSent = false;
101
- }
102
- this.eventCount++;
103
- if (this.eventCount <= this.threshold) {
104
- return 'deliver';
105
- }
106
- if (!this.overflowSent) {
107
- this.overflowSent = true;
108
- return 'overflow';
109
- }
110
- return 'drop';
111
- }
112
- }
113
- exports.EventThrottle = EventThrottle;
114
63
  function discoverRealtimeTables(build) {
115
64
  const { pgRegistry } = build.input;
116
65
  const resources = pgRegistry.pgResources;
@@ -163,44 +112,44 @@ function buildTypeDefs(tables) {
163
112
  .join('\n\n');
164
113
  return `extend type Subscription {\n${subscriptionFields}\n}\n\n${payloadTypes}`;
165
114
  }
115
+ /**
116
+ * The gate never emits a null payload, so one here means the plan graph was
117
+ * rewired wrongly — fail rather than invent an event for the client.
118
+ */
119
+ function requirePayload(payload) {
120
+ if (payload === null || payload === undefined) {
121
+ throw new Error('Realtime subscription payload is missing: the gated subscriber only ever ' +
122
+ 'yields parsed payloads, so this event bypassed createGatedSubscriber.');
123
+ }
124
+ return payload;
125
+ }
126
+ /**
127
+ * The row this event reports. `rowIds` is already narrowed to the
128
+ * subscription's `ids`, so the first entry is the one to surface; INVALIDATE
129
+ * carries none, which is a genuine absence rather than a suppressed error.
130
+ */
131
+ function reportedRowId(payload) {
132
+ if (payload.overflow)
133
+ return null;
134
+ return payload.rowIds[0] ?? null;
135
+ }
166
136
  function buildPlans(tables, overflowThreshold) {
167
137
  const subscriptionPlans = {};
168
138
  const allPlans = {};
169
139
  for (const { resource, fieldName, payloadTypeName, rowFieldName, notifyChannel } of tables) {
170
- const throttle = new EventThrottle(overflowThreshold);
171
140
  subscriptionPlans[fieldName] = {
172
141
  subscribePlan(_$root, args) {
173
142
  const $pgSubscriber = (0, grafast_1.context)().get('pgSubscriber');
174
143
  const $topic = (0, grafast_1.constant)(notifyChannel);
175
144
  const $ids = args.getRaw('ids');
176
- return (0, grafast_1.listen)($pgSubscriber, $topic, ($payload) => {
177
- const $parsed = (0, grafast_1.lambda)([$payload, $ids], (pair) => {
178
- const [raw, subscribedIds] = pair;
179
- const parsed = parseNotifyPayload(String(raw));
180
- const action = parsed.overflow ? 'deliver' : throttle.check();
181
- if (action === 'drop') {
182
- return null;
183
- }
184
- if (action === 'overflow') {
185
- return {
186
- event: 'INVALIDATE',
187
- rowIds: [],
188
- overflow: true,
189
- };
190
- }
191
- // Sparse set filtering: only deliver events for subscribed row IDs
192
- if (subscribedIds && subscribedIds.length > 0) {
193
- const hasMatch = parsed.rowIds.some((rid) => subscribedIds.includes(rid));
194
- if (!hasMatch)
195
- return null;
196
- }
197
- return parsed;
198
- });
199
- return (0, grafast_1.object)({
200
- parsed: $parsed,
201
- subscribedIds: $ids,
202
- });
145
+ // Parsing, throttling and sparse-set filtering all happen in the gate,
146
+ // built once per subscription, so the stream below yields only events
147
+ // that should reach this client — every step after it is total.
148
+ const $subscriber = (0, grafast_1.lambda)([$pgSubscriber, $ids], (pair) => {
149
+ const [pgSubscriber, ids] = pair;
150
+ return (0, event_gate_1.createGatedSubscriber)(pgSubscriber, { ids, threshold: overflowThreshold });
203
151
  });
152
+ return (0, grafast_1.listen)($subscriber, $topic, ($payload) => (0, grafast_1.object)({ parsed: $payload }));
204
153
  },
205
154
  plan($event) {
206
155
  return $event;
@@ -208,41 +157,16 @@ function buildPlans(tables, overflowThreshold) {
208
157
  };
209
158
  allPlans[payloadTypeName] = {
210
159
  event($parent) {
211
- const $parsed = $parent.get('parsed');
212
- return (0, grafast_1.lambda)($parsed, (p) => p?.event ?? 'UNKNOWN');
160
+ return (0, grafast_1.lambda)($parent.get('parsed'), (p) => requirePayload(p).event);
213
161
  },
214
162
  rowId($parent) {
215
- const $parsed = $parent.get('parsed');
216
- const $subscribedIds = $parent.get('subscribedIds');
217
- return (0, grafast_1.lambda)([$parsed, $subscribedIds], (pair) => {
218
- const [p, subscribedIds] = pair;
219
- if (!p || p.overflow || p.rowIds.length === 0)
220
- return null;
221
- // When ids are provided, return the first matching row ID
222
- if (subscribedIds && subscribedIds.length > 0) {
223
- return p.rowIds.find((rid) => subscribedIds.includes(rid)) ?? null;
224
- }
225
- return p.rowIds[0];
226
- });
163
+ return (0, grafast_1.lambda)($parent.get('parsed'), (p) => reportedRowId(requirePayload(p)));
227
164
  },
228
165
  overflow($parent) {
229
- const $parsed = $parent.get('parsed');
230
- return (0, grafast_1.lambda)($parsed, (p) => p?.overflow ?? false);
166
+ return (0, grafast_1.lambda)($parent.get('parsed'), (p) => requirePayload(p).overflow);
231
167
  },
232
168
  [rowFieldName]($parent) {
233
- const $parsed = $parent.get('parsed');
234
- const $subscribedIds = $parent.get('subscribedIds');
235
- const $rowId = (0, grafast_1.lambda)([$parsed, $subscribedIds], (tuple) => {
236
- const [p, subscribedIds] = tuple;
237
- if (!p || p.overflow || p.rowIds.length === 0)
238
- return null;
239
- // When ids are provided, return first matching row ID
240
- if (subscribedIds && subscribedIds.length > 0) {
241
- return p.rowIds.find((rid) => subscribedIds.includes(rid)) ?? null;
242
- }
243
- // Full collection mode: return first row ID
244
- return p.rowIds[0];
245
- });
169
+ const $rowId = (0, grafast_1.lambda)($parent.get('parsed'), (p) => reportedRowId(requirePayload(p)));
246
170
  return resource.get({ id: $rowId });
247
171
  },
248
172
  };
@@ -268,5 +192,10 @@ function createRealtimeSubscriptionsPlugin(options = {}) {
268
192
  // Re-export CursorTracker and RealtimeManager for convenience
269
193
  var cursor_tracker_1 = require("./cursor-tracker");
270
194
  Object.defineProperty(exports, "CursorTracker", { enumerable: true, get: function () { return cursor_tracker_1.CursorTracker; } });
195
+ var event_gate_2 = require("./event-gate");
196
+ Object.defineProperty(exports, "createGatedSubscriber", { enumerable: true, get: function () { return event_gate_2.createGatedSubscriber; } });
197
+ Object.defineProperty(exports, "EventThrottle", { enumerable: true, get: function () { return event_gate_2.EventThrottle; } });
198
+ Object.defineProperty(exports, "MalformedNotifyPayloadError", { enumerable: true, get: function () { return event_gate_2.MalformedNotifyPayloadError; } });
199
+ Object.defineProperty(exports, "parseNotifyPayload", { enumerable: true, get: function () { return event_gate_2.parseNotifyPayload; } });
271
200
  var realtime_manager_1 = require("./realtime-manager");
272
201
  Object.defineProperty(exports, "RealtimeManager", { enumerable: true, get: function () { return realtime_manager_1.RealtimeManager; } });