@hotmeshio/hotmesh 0.6.1 → 0.7.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 +179 -142
- package/build/modules/enums.d.ts +7 -0
- package/build/modules/enums.js +16 -1
- package/build/modules/utils.d.ts +27 -0
- package/build/modules/utils.js +52 -1
- package/build/package.json +10 -8
- package/build/services/connector/providers/postgres.js +3 -0
- package/build/services/hotmesh/index.d.ts +66 -15
- package/build/services/hotmesh/index.js +84 -15
- package/build/services/memflow/index.d.ts +100 -14
- package/build/services/memflow/index.js +100 -14
- package/build/services/memflow/worker.d.ts +97 -0
- package/build/services/memflow/worker.js +217 -0
- package/build/services/memflow/workflow/proxyActivities.d.ts +74 -3
- package/build/services/memflow/workflow/proxyActivities.js +81 -4
- package/build/services/router/consumption/index.d.ts +2 -1
- package/build/services/router/consumption/index.js +38 -2
- package/build/services/router/error-handling/index.d.ts +3 -3
- package/build/services/router/error-handling/index.js +48 -13
- package/build/services/router/index.d.ts +1 -0
- package/build/services/router/index.js +2 -1
- package/build/services/store/index.d.ts +3 -2
- package/build/services/store/providers/postgres/kvtypes/hash/basic.js +36 -6
- package/build/services/store/providers/postgres/kvtypes/hash/expire.js +12 -2
- package/build/services/store/providers/postgres/kvtypes/hash/scan.js +30 -10
- package/build/services/store/providers/postgres/kvtypes/list.js +68 -10
- package/build/services/store/providers/postgres/kvtypes/string.js +60 -10
- package/build/services/store/providers/postgres/kvtypes/zset.js +92 -22
- package/build/services/store/providers/postgres/postgres.d.ts +3 -3
- package/build/services/store/providers/redis/_base.d.ts +3 -3
- package/build/services/store/providers/redis/ioredis.js +17 -7
- package/build/services/stream/providers/postgres/kvtables.js +76 -23
- package/build/services/stream/providers/postgres/lifecycle.d.ts +19 -0
- package/build/services/stream/providers/postgres/lifecycle.js +54 -0
- package/build/services/stream/providers/postgres/messages.d.ts +56 -0
- package/build/services/stream/providers/postgres/messages.js +253 -0
- package/build/services/stream/providers/postgres/notifications.d.ts +59 -0
- package/build/services/stream/providers/postgres/notifications.js +357 -0
- package/build/services/stream/providers/postgres/postgres.d.ts +110 -11
- package/build/services/stream/providers/postgres/postgres.js +196 -488
- package/build/services/stream/providers/postgres/scout.d.ts +68 -0
- package/build/services/stream/providers/postgres/scout.js +233 -0
- package/build/services/stream/providers/postgres/stats.d.ts +49 -0
- package/build/services/stream/providers/postgres/stats.js +113 -0
- package/build/services/sub/providers/postgres/postgres.js +37 -5
- package/build/services/sub/providers/redis/ioredis.js +13 -2
- package/build/services/sub/providers/redis/redis.js +13 -2
- package/build/services/worker/index.d.ts +1 -0
- package/build/services/worker/index.js +2 -0
- package/build/types/hotmesh.d.ts +42 -2
- package/build/types/index.d.ts +3 -3
- package/build/types/memflow.d.ts +32 -0
- package/build/types/provider.d.ts +16 -0
- package/build/types/stream.d.ts +92 -1
- package/package.json +10 -8
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getNotificationTimeout = exports.getFallbackInterval = exports.NotificationManager = void 0;
|
|
4
|
+
const enums_1 = require("../../../../modules/enums");
|
|
5
|
+
const kvtables_1 = require("./kvtables");
|
|
6
|
+
/**
|
|
7
|
+
* Manages PostgreSQL LISTEN/NOTIFY for stream message notifications.
|
|
8
|
+
* Handles static state shared across all service instances using the same client.
|
|
9
|
+
*/
|
|
10
|
+
class NotificationManager {
|
|
11
|
+
constructor(client, getTableName, getFallbackInterval, logger) {
|
|
12
|
+
this.client = client;
|
|
13
|
+
this.getTableName = getTableName;
|
|
14
|
+
this.getFallbackInterval = getFallbackInterval;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
// Instance-level tracking
|
|
17
|
+
this.instanceNotificationConsumers = new Set();
|
|
18
|
+
this.notificationHandlerBound = this.handleNotification.bind(this);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Set up notification handler for this client (once per client).
|
|
22
|
+
*/
|
|
23
|
+
setupClientNotificationHandler(serviceInstance) {
|
|
24
|
+
if (NotificationManager.clientNotificationHandlers.get(this.client)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// Initialize notification consumer map for this client
|
|
28
|
+
if (!NotificationManager.clientNotificationConsumers.has(this.client)) {
|
|
29
|
+
NotificationManager.clientNotificationConsumers.set(this.client, new Map());
|
|
30
|
+
}
|
|
31
|
+
// Set up the notification handler
|
|
32
|
+
this.client.on('notification', this.notificationHandlerBound);
|
|
33
|
+
// Mark this client as having a notification handler
|
|
34
|
+
NotificationManager.clientNotificationHandlers.set(this.client, true);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Start fallback poller for missed notifications (once per client).
|
|
38
|
+
*/
|
|
39
|
+
startClientFallbackPoller(checkForMissedMessages) {
|
|
40
|
+
if (NotificationManager.clientFallbackPollers.has(this.client)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const interval = this.getFallbackInterval();
|
|
44
|
+
const fallbackIntervalId = setInterval(() => {
|
|
45
|
+
checkForMissedMessages().catch((error) => {
|
|
46
|
+
this.logger.error('postgres-stream-fallback-poller-error', { error });
|
|
47
|
+
});
|
|
48
|
+
}, interval);
|
|
49
|
+
NotificationManager.clientFallbackPollers.set(this.client, fallbackIntervalId);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check for missed messages (fallback polling).
|
|
53
|
+
* Handles errors gracefully to avoid noise during shutdown.
|
|
54
|
+
*/
|
|
55
|
+
async checkForMissedMessages(fetchMessages) {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
// Check for visible messages using notify_visible_messages function
|
|
58
|
+
try {
|
|
59
|
+
const tableName = this.getTableName();
|
|
60
|
+
const schemaName = tableName.split('.')[0];
|
|
61
|
+
const result = await this.client.query(`SELECT ${schemaName}.notify_visible_messages() as count`);
|
|
62
|
+
const notificationCount = result.rows[0]?.count || 0;
|
|
63
|
+
if (notificationCount > 0) {
|
|
64
|
+
this.logger.info('postgres-stream-visibility-notifications', {
|
|
65
|
+
count: notificationCount,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
// Silently ignore errors during shutdown (client closed, etc.)
|
|
71
|
+
// Function might not exist in older schemas
|
|
72
|
+
if (error.message?.includes('Client was closed')) {
|
|
73
|
+
// Client is shutting down, silently return
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.logger.debug('postgres-stream-visibility-function-unavailable', {
|
|
77
|
+
error: error.message,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Traditional fallback check for active notification consumers
|
|
81
|
+
const clientNotificationConsumers = NotificationManager.clientNotificationConsumers.get(this.client);
|
|
82
|
+
if (!clientNotificationConsumers) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Check consumers that haven't been checked recently
|
|
86
|
+
for (const [consumerKey, instanceMap,] of clientNotificationConsumers.entries()) {
|
|
87
|
+
for (const [instance, consumer] of instanceMap.entries()) {
|
|
88
|
+
if (consumer.isListening &&
|
|
89
|
+
now - consumer.lastFallbackCheck > this.getFallbackInterval()) {
|
|
90
|
+
try {
|
|
91
|
+
const messages = await fetchMessages(instance, consumer);
|
|
92
|
+
if (messages.length > 0) {
|
|
93
|
+
this.logger.debug('postgres-stream-fallback-messages', {
|
|
94
|
+
streamName: consumer.streamName,
|
|
95
|
+
groupName: consumer.groupName,
|
|
96
|
+
messageCount: messages.length,
|
|
97
|
+
});
|
|
98
|
+
consumer.callback(messages);
|
|
99
|
+
}
|
|
100
|
+
consumer.lastFallbackCheck = now;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// Silently ignore errors during shutdown
|
|
104
|
+
if (error.message?.includes('Client was closed')) {
|
|
105
|
+
// Client is shutting down, stop checking this consumer
|
|
106
|
+
consumer.isListening = false;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.logger.error('postgres-stream-fallback-error', {
|
|
110
|
+
streamName: consumer.streamName,
|
|
111
|
+
groupName: consumer.groupName,
|
|
112
|
+
error,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Handle incoming PostgreSQL notification.
|
|
121
|
+
*/
|
|
122
|
+
handleNotification(notification) {
|
|
123
|
+
try {
|
|
124
|
+
// Only handle stream notifications
|
|
125
|
+
if (!notification.channel.startsWith('stream_')) {
|
|
126
|
+
this.logger.debug('postgres-stream-ignoring-sub-notification', {
|
|
127
|
+
channel: notification.channel,
|
|
128
|
+
payloadPreview: notification.payload.substring(0, 100),
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.logger.debug('postgres-stream-processing-notification', {
|
|
133
|
+
channel: notification.channel,
|
|
134
|
+
});
|
|
135
|
+
const payload = JSON.parse(notification.payload);
|
|
136
|
+
const { stream_name, group_name } = payload;
|
|
137
|
+
if (!stream_name || !group_name) {
|
|
138
|
+
this.logger.warn('postgres-stream-invalid-notification', {
|
|
139
|
+
notification,
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const consumerKey = this.getConsumerKey(stream_name, group_name);
|
|
144
|
+
const clientNotificationConsumers = NotificationManager.clientNotificationConsumers.get(this.client);
|
|
145
|
+
if (!clientNotificationConsumers) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const instanceMap = clientNotificationConsumers.get(consumerKey);
|
|
149
|
+
if (!instanceMap) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Trigger immediate message fetch for all instances with this consumer
|
|
153
|
+
for (const [instance, consumer] of instanceMap.entries()) {
|
|
154
|
+
if (consumer.isListening) {
|
|
155
|
+
const serviceInstance = instance;
|
|
156
|
+
if (serviceInstance.fetchAndDeliverMessages) {
|
|
157
|
+
serviceInstance.fetchAndDeliverMessages(consumer);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
this.logger.error('postgres-stream-notification-parse-error', {
|
|
164
|
+
notification,
|
|
165
|
+
error,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Set up notification consumer for a stream/group.
|
|
171
|
+
*/
|
|
172
|
+
async setupNotificationConsumer(serviceInstance, streamName, groupName, consumerName, callback) {
|
|
173
|
+
const startTime = Date.now();
|
|
174
|
+
const consumerKey = this.getConsumerKey(streamName, groupName);
|
|
175
|
+
const channelName = (0, kvtables_1.getNotificationChannelName)(streamName, groupName);
|
|
176
|
+
// Get or create notification consumer map for this client
|
|
177
|
+
let clientNotificationConsumers = NotificationManager.clientNotificationConsumers.get(this.client);
|
|
178
|
+
if (!clientNotificationConsumers) {
|
|
179
|
+
clientNotificationConsumers = new Map();
|
|
180
|
+
NotificationManager.clientNotificationConsumers.set(this.client, clientNotificationConsumers);
|
|
181
|
+
}
|
|
182
|
+
// Get or create instance map for this consumer key
|
|
183
|
+
let instanceMap = clientNotificationConsumers.get(consumerKey);
|
|
184
|
+
if (!instanceMap) {
|
|
185
|
+
instanceMap = new Map();
|
|
186
|
+
clientNotificationConsumers.set(consumerKey, instanceMap);
|
|
187
|
+
// Set up LISTEN for this channel (only once per channel)
|
|
188
|
+
try {
|
|
189
|
+
const listenStart = Date.now();
|
|
190
|
+
await this.client.query(`LISTEN "${channelName}"`);
|
|
191
|
+
this.logger.debug('postgres-stream-listen-start', {
|
|
192
|
+
streamName,
|
|
193
|
+
groupName,
|
|
194
|
+
channelName,
|
|
195
|
+
listenDuration: Date.now() - listenStart,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
this.logger.error('postgres-stream-listen-error', {
|
|
200
|
+
streamName,
|
|
201
|
+
groupName,
|
|
202
|
+
channelName,
|
|
203
|
+
error,
|
|
204
|
+
});
|
|
205
|
+
throw error; // Propagate error to caller
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Register consumer for this instance
|
|
209
|
+
const consumer = {
|
|
210
|
+
streamName,
|
|
211
|
+
groupName,
|
|
212
|
+
consumerName,
|
|
213
|
+
callback,
|
|
214
|
+
isListening: true,
|
|
215
|
+
lastFallbackCheck: Date.now(),
|
|
216
|
+
};
|
|
217
|
+
instanceMap.set(serviceInstance, consumer);
|
|
218
|
+
// Track this consumer for cleanup
|
|
219
|
+
this.instanceNotificationConsumers.add(consumerKey);
|
|
220
|
+
this.logger.debug('postgres-stream-notification-setup-complete', {
|
|
221
|
+
streamName,
|
|
222
|
+
groupName,
|
|
223
|
+
instanceCount: instanceMap.size,
|
|
224
|
+
setupDuration: Date.now() - startTime,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Stop notification consumer for a stream/group.
|
|
229
|
+
*/
|
|
230
|
+
async stopNotificationConsumer(serviceInstance, streamName, groupName) {
|
|
231
|
+
const consumerKey = this.getConsumerKey(streamName, groupName);
|
|
232
|
+
const clientNotificationConsumers = NotificationManager.clientNotificationConsumers.get(this.client);
|
|
233
|
+
if (!clientNotificationConsumers) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const instanceMap = clientNotificationConsumers.get(consumerKey);
|
|
237
|
+
if (!instanceMap) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const consumer = instanceMap.get(serviceInstance);
|
|
241
|
+
if (consumer) {
|
|
242
|
+
consumer.isListening = false;
|
|
243
|
+
instanceMap.delete(serviceInstance);
|
|
244
|
+
// Remove from instance tracking
|
|
245
|
+
this.instanceNotificationConsumers.delete(consumerKey);
|
|
246
|
+
// If no more instances for this consumer key, stop listening
|
|
247
|
+
if (instanceMap.size === 0) {
|
|
248
|
+
clientNotificationConsumers.delete(consumerKey);
|
|
249
|
+
const channelName = (0, kvtables_1.getNotificationChannelName)(streamName, groupName);
|
|
250
|
+
try {
|
|
251
|
+
await this.client.query(`UNLISTEN "${channelName}"`);
|
|
252
|
+
this.logger.debug('postgres-stream-unlisten', {
|
|
253
|
+
streamName,
|
|
254
|
+
groupName,
|
|
255
|
+
channelName,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
this.logger.error('postgres-stream-unlisten-error', {
|
|
260
|
+
streamName,
|
|
261
|
+
groupName,
|
|
262
|
+
channelName,
|
|
263
|
+
error,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Clean up notification consumers for this instance.
|
|
271
|
+
* Stops fallback poller FIRST to prevent race conditions during shutdown.
|
|
272
|
+
*/
|
|
273
|
+
async cleanup(serviceInstance) {
|
|
274
|
+
const clientNotificationConsumers = NotificationManager.clientNotificationConsumers.get(this.client);
|
|
275
|
+
// FIRST: Stop fallback poller to prevent queries during cleanup
|
|
276
|
+
const fallbackIntervalId = NotificationManager.clientFallbackPollers.get(this.client);
|
|
277
|
+
if (fallbackIntervalId) {
|
|
278
|
+
clearInterval(fallbackIntervalId);
|
|
279
|
+
NotificationManager.clientFallbackPollers.delete(this.client);
|
|
280
|
+
}
|
|
281
|
+
if (clientNotificationConsumers) {
|
|
282
|
+
// Remove this instance from all consumer maps
|
|
283
|
+
for (const consumerKey of this.instanceNotificationConsumers) {
|
|
284
|
+
const instanceMap = clientNotificationConsumers.get(consumerKey);
|
|
285
|
+
if (instanceMap) {
|
|
286
|
+
const consumer = instanceMap.get(serviceInstance);
|
|
287
|
+
if (consumer) {
|
|
288
|
+
consumer.isListening = false;
|
|
289
|
+
instanceMap.delete(serviceInstance);
|
|
290
|
+
// If no more instances for this consumer, stop listening
|
|
291
|
+
if (instanceMap.size === 0) {
|
|
292
|
+
clientNotificationConsumers.delete(consumerKey);
|
|
293
|
+
const channelName = (0, kvtables_1.getNotificationChannelName)(consumer.streamName, consumer.groupName);
|
|
294
|
+
try {
|
|
295
|
+
await this.client.query(`UNLISTEN "${channelName}"`);
|
|
296
|
+
this.logger.debug('postgres-stream-cleanup-unlisten', {
|
|
297
|
+
streamName: consumer.streamName,
|
|
298
|
+
groupName: consumer.groupName,
|
|
299
|
+
channelName,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
// Silently ignore errors during shutdown
|
|
304
|
+
if (!error.message?.includes('Client was closed')) {
|
|
305
|
+
this.logger.error('postgres-stream-cleanup-unlisten-error', {
|
|
306
|
+
streamName: consumer.streamName,
|
|
307
|
+
groupName: consumer.groupName,
|
|
308
|
+
channelName,
|
|
309
|
+
error,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Clear instance tracking
|
|
319
|
+
this.instanceNotificationConsumers.clear();
|
|
320
|
+
// If no more consumers exist for this client, clean up static resources
|
|
321
|
+
if (clientNotificationConsumers && clientNotificationConsumers.size === 0) {
|
|
322
|
+
// Remove client from static maps
|
|
323
|
+
NotificationManager.clientNotificationConsumers.delete(this.client);
|
|
324
|
+
NotificationManager.clientNotificationHandlers.delete(this.client);
|
|
325
|
+
// Fallback poller already stopped above
|
|
326
|
+
// Remove notification handler
|
|
327
|
+
if (this.client.removeAllListeners) {
|
|
328
|
+
this.client.removeAllListeners('notification');
|
|
329
|
+
}
|
|
330
|
+
else if (this.client.off && this.notificationHandlerBound) {
|
|
331
|
+
this.client.off('notification', this.notificationHandlerBound);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get consumer key from stream and group names.
|
|
337
|
+
*/
|
|
338
|
+
getConsumerKey(streamName, groupName) {
|
|
339
|
+
return `${streamName}:${groupName}`;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Static maps shared across all instances with the same client
|
|
343
|
+
NotificationManager.clientNotificationConsumers = new Map();
|
|
344
|
+
NotificationManager.clientNotificationHandlers = new Map();
|
|
345
|
+
NotificationManager.clientFallbackPollers = new Map();
|
|
346
|
+
exports.NotificationManager = NotificationManager;
|
|
347
|
+
/**
|
|
348
|
+
* Get configuration values for notification settings.
|
|
349
|
+
*/
|
|
350
|
+
function getFallbackInterval(config) {
|
|
351
|
+
return config?.postgres?.notificationFallbackInterval || enums_1.HMSH_ROUTER_POLL_FALLBACK_INTERVAL;
|
|
352
|
+
}
|
|
353
|
+
exports.getFallbackInterval = getFallbackInterval;
|
|
354
|
+
function getNotificationTimeout(config) {
|
|
355
|
+
return config?.postgres?.notificationTimeout || 5000; // Default: 5 seconds
|
|
356
|
+
}
|
|
357
|
+
exports.getNotificationTimeout = getNotificationTimeout;
|
|
@@ -5,24 +5,123 @@ import { KeyStoreParams, StringAnyType } from '../../../../types';
|
|
|
5
5
|
import { PostgresClientType } from '../../../../types/postgres';
|
|
6
6
|
import { PublishMessageConfig, StreamConfig, StreamMessage, StreamStats } from '../../../../types/stream';
|
|
7
7
|
import { ProviderClient, ProviderTransaction } from '../../../../types/provider';
|
|
8
|
+
/**
|
|
9
|
+
* PostgreSQL Stream Service
|
|
10
|
+
*
|
|
11
|
+
* High-performance stream message provider using PostgreSQL with LISTEN/NOTIFY.
|
|
12
|
+
*
|
|
13
|
+
* ## Module Organization
|
|
14
|
+
*
|
|
15
|
+
* This service is organized into focused modules following KISS principles:
|
|
16
|
+
* - `postgres.ts` (this file) - Main orchestrator and service interface
|
|
17
|
+
* - `kvtables.ts` - Schema deployment and table management
|
|
18
|
+
* - `messages.ts` - Message CRUD operations (publish, fetch, ack, delete)
|
|
19
|
+
* - `stats.ts` - Statistics and query operations
|
|
20
|
+
* - `scout.ts` - Scout role coordination for polling visible messages
|
|
21
|
+
* - `notifications.ts` - LISTEN/NOTIFY notification system with static state management
|
|
22
|
+
* - `lifecycle.ts` - Stream and consumer group lifecycle operations
|
|
23
|
+
*
|
|
24
|
+
* ## Lifecycle
|
|
25
|
+
*
|
|
26
|
+
* ### Initialization (`init`)
|
|
27
|
+
* 1. Deploy PostgreSQL schema (tables, indexes, triggers, functions)
|
|
28
|
+
* 2. Create ScoutManager for coordinating visibility timeout polling
|
|
29
|
+
* 3. Create NotificationManager for LISTEN/NOTIFY event handling
|
|
30
|
+
* 4. Set up notification handler (once per client, shared across instances)
|
|
31
|
+
* 5. Start fallback poller (backup for missed notifications)
|
|
32
|
+
* 6. Start router scout poller (for visibility timeout processing)
|
|
33
|
+
*
|
|
34
|
+
* ### Shutdown (`cleanup`)
|
|
35
|
+
* 1. Stop router scout polling loop
|
|
36
|
+
* 2. Release scout role if held
|
|
37
|
+
* 3. Stop notification consumers for this instance
|
|
38
|
+
* 4. UNLISTEN from channels when last instance disconnects
|
|
39
|
+
* 5. Clean up fallback poller when last instance disconnects
|
|
40
|
+
* 6. Remove notification handlers when last instance disconnects
|
|
41
|
+
*
|
|
42
|
+
* ## Notification System (LISTEN/NOTIFY)
|
|
43
|
+
*
|
|
44
|
+
* ### Real-time Message Delivery
|
|
45
|
+
* - PostgreSQL trigger on INSERT sends NOTIFY when messages are immediately visible
|
|
46
|
+
* - Messages with visibility timeout are NOT notified on INSERT
|
|
47
|
+
* - Multiple service instances share the same client and notification handlers
|
|
48
|
+
* - Static state ensures only ONE LISTEN per channel across all instances
|
|
49
|
+
*
|
|
50
|
+
* ### Components
|
|
51
|
+
* - **Notification Handler**: Listens for PostgreSQL NOTIFY events
|
|
52
|
+
* - **Fallback Poller**: Polls every 30s (default) for missed messages
|
|
53
|
+
* - **Router Scout**: Active role-holder polls visible messages frequently (~100ms)
|
|
54
|
+
* - **Visibility Function**: `notify_visible_messages()` checks for expired timeouts
|
|
55
|
+
*
|
|
56
|
+
* ## Scout Role (Visibility Timeout Processing)
|
|
57
|
+
*
|
|
58
|
+
* When messages are published with visibility timeouts (delays), they need to be
|
|
59
|
+
* processed when they become visible. The scout role ensures this happens efficiently:
|
|
60
|
+
*
|
|
61
|
+
* 1. **Role Acquisition**: One instance per app acquires "router" scout role
|
|
62
|
+
* 2. **Fast Polling**: Scout polls `notify_visible_messages()` every ~100ms
|
|
63
|
+
* 3. **Notification**: Function triggers NOTIFY for streams with visible messages
|
|
64
|
+
* 4. **Role Rotation**: Role expires after interval, another instance can claim it
|
|
65
|
+
* 5. **Fallback**: Non-scouts sleep longer, try to acquire role periodically
|
|
66
|
+
*
|
|
67
|
+
* ## Message Flow
|
|
68
|
+
*
|
|
69
|
+
* ### Publishing
|
|
70
|
+
* 1. Messages inserted into partitioned table
|
|
71
|
+
* 2. If immediately visible → INSERT trigger sends NOTIFY
|
|
72
|
+
* 3. If visibility timeout → no NOTIFY (scout will handle when visible)
|
|
73
|
+
*
|
|
74
|
+
* ### Consuming (Event-Driven)
|
|
75
|
+
* 1. Consumer calls `consumeMessages` with notification callback
|
|
76
|
+
* 2. Service executes LISTEN on channel `stream_{name}_{group}`
|
|
77
|
+
* 3. On NOTIFY → fetch messages → invoke callback
|
|
78
|
+
* 4. Initial fetch done immediately (catch any queued messages)
|
|
79
|
+
*
|
|
80
|
+
* ### Consuming (Polling)
|
|
81
|
+
* 1. Consumer calls `consumeMessages` without callback
|
|
82
|
+
* 2. Service directly queries and reserves messages
|
|
83
|
+
* 3. Returns messages synchronously
|
|
84
|
+
*
|
|
85
|
+
* ## Reliability Guarantees
|
|
86
|
+
*
|
|
87
|
+
* - **Notification Fallback**: Poller catches missed notifications every 30s
|
|
88
|
+
* - **Visibility Scout**: Ensures delayed messages are processed when visible
|
|
89
|
+
* - **Graceful Degradation**: Falls back to polling if LISTEN fails
|
|
90
|
+
* - **Shared State**: Multiple instances coordinate via static maps
|
|
91
|
+
* - **Race Condition Safe**: SKIP LOCKED prevents message duplication
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* // Initialize service
|
|
96
|
+
* const service = new PostgresStreamService(client, storeClient, config);
|
|
97
|
+
* await service.init('namespace', 'appId', logger);
|
|
98
|
+
*
|
|
99
|
+
* // Event-driven consumption (recommended)
|
|
100
|
+
* await service.consumeMessages('stream', 'group', 'consumer', {
|
|
101
|
+
* notificationCallback: (messages) => {
|
|
102
|
+
* // Process messages in real-time
|
|
103
|
+
* }
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* // Polling consumption
|
|
107
|
+
* const messages = await service.consumeMessages('stream', 'group', 'consumer', {
|
|
108
|
+
* batchSize: 10
|
|
109
|
+
* });
|
|
110
|
+
*
|
|
111
|
+
* // Cleanup on shutdown
|
|
112
|
+
* await service.cleanup();
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
8
115
|
declare class PostgresStreamService extends StreamService<PostgresClientType & ProviderClient, any> {
|
|
9
116
|
namespace: string;
|
|
10
117
|
appId: string;
|
|
11
118
|
logger: ILogger;
|
|
12
|
-
private
|
|
13
|
-
private
|
|
14
|
-
private static clientFallbackPollers;
|
|
15
|
-
private instanceNotificationConsumers;
|
|
16
|
-
private notificationHandlerBound;
|
|
119
|
+
private scoutManager;
|
|
120
|
+
private notificationManager;
|
|
17
121
|
constructor(streamClient: PostgresClientType & ProviderClient, storeClient: ProviderClient, config?: StreamConfig);
|
|
18
122
|
init(namespace: string, appId: string, logger: ILogger): Promise<void>;
|
|
19
|
-
private setupClientNotificationHandler;
|
|
20
|
-
private startClientFallbackPoller;
|
|
21
123
|
private isNotificationsEnabled;
|
|
22
|
-
private getFallbackInterval;
|
|
23
|
-
private getNotificationTimeout;
|
|
24
124
|
private checkForMissedMessages;
|
|
25
|
-
private handleNotification;
|
|
26
125
|
private fetchAndDeliverMessages;
|
|
27
126
|
private getConsumerKey;
|
|
28
127
|
mintKey(type: KeyType, params: KeyStoreParams): string;
|
|
@@ -51,7 +150,7 @@ declare class PostgresStreamService extends StreamService<PostgresClientType & P
|
|
|
51
150
|
* allows calls to the stream to be roped into a single SQL transaction.
|
|
52
151
|
*/
|
|
53
152
|
publishMessages(streamName: string, messages: string[], options?: PublishMessageConfig): Promise<string[] | ProviderTransaction>;
|
|
54
|
-
_publishMessages(streamName: string, messages: string[]): {
|
|
153
|
+
_publishMessages(streamName: string, messages: string[], options?: PublishMessageConfig): {
|
|
55
154
|
sql: string;
|
|
56
155
|
params: any[];
|
|
57
156
|
};
|