@hotmeshio/hotmesh 0.6.1 → 0.8.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/.claude/settings.local.json +7 -0
- package/README.md +179 -142
- package/build/index.d.ts +1 -3
- package/build/index.js +1 -5
- 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 +55 -32
- package/build/package.json +18 -27
- package/build/services/activities/activity.d.ts +43 -6
- package/build/services/activities/activity.js +262 -54
- package/build/services/activities/await.js +2 -2
- package/build/services/activities/cycle.js +1 -1
- package/build/services/activities/hook.d.ts +5 -0
- package/build/services/activities/hook.js +22 -19
- package/build/services/activities/interrupt.js +17 -25
- package/build/services/activities/signal.d.ts +4 -2
- package/build/services/activities/signal.js +27 -24
- package/build/services/activities/worker.js +2 -2
- package/build/services/collator/index.d.ts +123 -25
- package/build/services/collator/index.js +224 -101
- package/build/services/connector/factory.d.ts +1 -1
- package/build/services/connector/factory.js +1 -11
- package/build/services/connector/providers/postgres.js +3 -0
- package/build/services/engine/index.d.ts +5 -5
- package/build/services/engine/index.js +36 -15
- 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 +39 -3
- 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/search/factory.js +1 -9
- package/build/services/store/factory.js +1 -9
- package/build/services/store/index.d.ts +8 -2
- package/build/services/store/providers/postgres/kvsql.d.ts +4 -0
- package/build/services/store/providers/postgres/kvsql.js +4 -0
- package/build/services/store/providers/postgres/kvtransaction.d.ts +2 -0
- package/build/services/store/providers/postgres/kvtransaction.js +23 -0
- package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +51 -0
- package/build/services/store/providers/postgres/kvtypes/hash/basic.js +229 -7
- package/build/services/store/providers/postgres/kvtypes/hash/expire.js +12 -2
- package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +4 -0
- package/build/services/store/providers/postgres/kvtypes/hash/index.js +6 -0
- 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 +23 -3
- package/build/services/store/providers/postgres/postgres.js +38 -1
- package/build/services/stream/factory.js +1 -17
- 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/factory.js +1 -9
- package/build/services/sub/index.d.ts +1 -1
- package/build/services/sub/providers/postgres/postgres.d.ts +1 -1
- package/build/services/sub/providers/postgres/postgres.js +53 -6
- package/build/services/task/index.d.ts +1 -1
- package/build/services/task/index.js +2 -6
- 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 -4
- package/build/types/index.js +1 -4
- package/build/types/memflow.d.ts +32 -0
- package/build/types/provider.d.ts +17 -1
- package/build/types/stream.d.ts +92 -1
- package/index.ts +0 -4
- package/package.json +18 -27
- package/build/services/connector/providers/ioredis.d.ts +0 -9
- package/build/services/connector/providers/ioredis.js +0 -26
- package/build/services/connector/providers/redis.d.ts +0 -9
- package/build/services/connector/providers/redis.js +0 -38
- package/build/services/search/providers/redis/ioredis.d.ts +0 -23
- package/build/services/search/providers/redis/ioredis.js +0 -189
- package/build/services/search/providers/redis/redis.d.ts +0 -23
- package/build/services/search/providers/redis/redis.js +0 -202
- package/build/services/store/providers/redis/_base.d.ts +0 -137
- package/build/services/store/providers/redis/_base.js +0 -980
- package/build/services/store/providers/redis/ioredis.d.ts +0 -20
- package/build/services/store/providers/redis/ioredis.js +0 -180
- package/build/services/store/providers/redis/redis.d.ts +0 -18
- package/build/services/store/providers/redis/redis.js +0 -199
- package/build/services/stream/providers/redis/ioredis.d.ts +0 -61
- package/build/services/stream/providers/redis/ioredis.js +0 -272
- package/build/services/stream/providers/redis/redis.d.ts +0 -61
- package/build/services/stream/providers/redis/redis.js +0 -305
- package/build/services/sub/providers/redis/ioredis.d.ts +0 -20
- package/build/services/sub/providers/redis/ioredis.js +0 -150
- package/build/services/sub/providers/redis/redis.d.ts +0 -18
- package/build/services/sub/providers/redis/redis.js +0 -137
- package/build/types/redis.d.ts +0 -258
- package/build/types/redis.js +0 -11
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ILogger } from '../../../logger';
|
|
2
|
+
import { KeyType } from '../../../../modules/key';
|
|
3
|
+
import { KeyStoreParams } from '../../../../types';
|
|
4
|
+
import { PostgresClientType } from '../../../../types/postgres';
|
|
5
|
+
import { ProviderClient } from '../../../../types/provider';
|
|
6
|
+
/**
|
|
7
|
+
* Scout state manager for coordinating polling across multiple instances.
|
|
8
|
+
* Only one instance at a time should be the active scout.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ScoutManager {
|
|
11
|
+
private client;
|
|
12
|
+
private appId;
|
|
13
|
+
private getTableName;
|
|
14
|
+
private mintKey;
|
|
15
|
+
private logger;
|
|
16
|
+
private isScout;
|
|
17
|
+
private shouldStopScout;
|
|
18
|
+
private pollCount;
|
|
19
|
+
private totalNotifications;
|
|
20
|
+
private scoutStartTime;
|
|
21
|
+
constructor(client: PostgresClientType & ProviderClient, appId: string, getTableName: () => string, mintKey: (type: KeyType, params: KeyStoreParams) => string, logger: ILogger);
|
|
22
|
+
/**
|
|
23
|
+
* Start the router scout polling loop.
|
|
24
|
+
* Winner polls frequently for visible messages, losers retry acquiring role less frequently.
|
|
25
|
+
*/
|
|
26
|
+
startRouterScoutPoller(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Stop the router scout polling loop and release the role.
|
|
29
|
+
*/
|
|
30
|
+
stopRouterScoutPoller(): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Check if this instance should act as the router scout.
|
|
33
|
+
*/
|
|
34
|
+
private shouldScout;
|
|
35
|
+
/**
|
|
36
|
+
* Main polling loop for the router scout.
|
|
37
|
+
*/
|
|
38
|
+
private pollForVisibleMessagesLoop;
|
|
39
|
+
/**
|
|
40
|
+
* Poll for visible messages and trigger notifications for any found.
|
|
41
|
+
*/
|
|
42
|
+
private pollForVisibleMessages;
|
|
43
|
+
/**
|
|
44
|
+
* Reserve the router scout role using direct SQL.
|
|
45
|
+
*/
|
|
46
|
+
private reserveRouterScoutRole;
|
|
47
|
+
/**
|
|
48
|
+
* Reserve a scout role for the specified type.
|
|
49
|
+
* Uses SET NX (set if not exists) with expiration to ensure only one instance holds the role.
|
|
50
|
+
*/
|
|
51
|
+
private reserveScoutRole;
|
|
52
|
+
/**
|
|
53
|
+
* Release a scout role for the specified type.
|
|
54
|
+
*/
|
|
55
|
+
private releaseScoutRole;
|
|
56
|
+
/**
|
|
57
|
+
* Get the router scout polling interval in milliseconds.
|
|
58
|
+
*/
|
|
59
|
+
private getRouterScoutInterval;
|
|
60
|
+
/**
|
|
61
|
+
* Check if this instance is currently the scout.
|
|
62
|
+
*/
|
|
63
|
+
isCurrentlyScout(): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Log polling metrics for this instance's scout tenure.
|
|
66
|
+
*/
|
|
67
|
+
private logPollingMetrics;
|
|
68
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ScoutManager = void 0;
|
|
4
|
+
const key_1 = require("../../../../modules/key");
|
|
5
|
+
const utils_1 = require("../../../../modules/utils");
|
|
6
|
+
const enums_1 = require("../../../../modules/enums");
|
|
7
|
+
/**
|
|
8
|
+
* Scout state manager for coordinating polling across multiple instances.
|
|
9
|
+
* Only one instance at a time should be the active scout.
|
|
10
|
+
*/
|
|
11
|
+
class ScoutManager {
|
|
12
|
+
constructor(client, appId, getTableName, mintKey, logger) {
|
|
13
|
+
this.client = client;
|
|
14
|
+
this.appId = appId;
|
|
15
|
+
this.getTableName = getTableName;
|
|
16
|
+
this.mintKey = mintKey;
|
|
17
|
+
this.logger = logger;
|
|
18
|
+
this.isScout = false;
|
|
19
|
+
this.shouldStopScout = false;
|
|
20
|
+
// Polling metrics
|
|
21
|
+
this.pollCount = 0;
|
|
22
|
+
this.totalNotifications = 0;
|
|
23
|
+
this.scoutStartTime = null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Start the router scout polling loop.
|
|
27
|
+
* Winner polls frequently for visible messages, losers retry acquiring role less frequently.
|
|
28
|
+
*/
|
|
29
|
+
startRouterScoutPoller() {
|
|
30
|
+
this.shouldStopScout = false;
|
|
31
|
+
this.pollForVisibleMessagesLoop().catch((error) => {
|
|
32
|
+
this.logger.error('postgres-stream-router-scout-start-error', { error });
|
|
33
|
+
});
|
|
34
|
+
this.logger.debug('postgres-stream-router-scout-started', {
|
|
35
|
+
appId: this.appId,
|
|
36
|
+
pollInterval: this.getRouterScoutInterval(),
|
|
37
|
+
scoutInterval: enums_1.HMSH_ROUTER_SCOUT_INTERVAL_SECONDS,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Stop the router scout polling loop and release the role.
|
|
42
|
+
*/
|
|
43
|
+
async stopRouterScoutPoller() {
|
|
44
|
+
this.shouldStopScout = true;
|
|
45
|
+
if (this.isScout) {
|
|
46
|
+
await this.releaseScoutRole('router');
|
|
47
|
+
this.isScout = false;
|
|
48
|
+
}
|
|
49
|
+
// Log polling metrics on shutdown
|
|
50
|
+
this.logPollingMetrics();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Check if this instance should act as the router scout.
|
|
54
|
+
*/
|
|
55
|
+
async shouldScout() {
|
|
56
|
+
const wasScout = this.isScout;
|
|
57
|
+
const isScout = wasScout || (this.isScout = await this.reserveRouterScoutRole());
|
|
58
|
+
if (isScout) {
|
|
59
|
+
if (!wasScout) {
|
|
60
|
+
// First time becoming scout - set timeout to reset after interval and track start time
|
|
61
|
+
this.scoutStartTime = Date.now();
|
|
62
|
+
this.logger.info('postgres-stream-router-scout-role-acquired', {
|
|
63
|
+
appId: this.appId,
|
|
64
|
+
});
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
this.isScout = false;
|
|
67
|
+
this.scoutStartTime = null;
|
|
68
|
+
}, enums_1.HMSH_ROUTER_SCOUT_INTERVAL_SECONDS * 1000);
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Main polling loop for the router scout.
|
|
76
|
+
*/
|
|
77
|
+
async pollForVisibleMessagesLoop() {
|
|
78
|
+
while (!this.shouldStopScout) {
|
|
79
|
+
try {
|
|
80
|
+
if (await this.shouldScout()) {
|
|
81
|
+
// We're the scout - poll for visible messages frequently
|
|
82
|
+
await this.pollForVisibleMessages();
|
|
83
|
+
// Sleep for the short polling interval
|
|
84
|
+
await (0, utils_1.sleepFor)(this.getRouterScoutInterval());
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// Not the scout - sleep longer before trying to acquire role again
|
|
88
|
+
await (0, utils_1.sleepFor)(enums_1.HMSH_ROUTER_SCOUT_INTERVAL_SECONDS * 1000);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
this.logger.error('postgres-stream-router-scout-loop-error', { error });
|
|
93
|
+
await (0, utils_1.sleepFor)(1000); // Brief pause on error
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Poll for visible messages and trigger notifications for any found.
|
|
99
|
+
*/
|
|
100
|
+
async pollForVisibleMessages() {
|
|
101
|
+
try {
|
|
102
|
+
const tableName = this.getTableName();
|
|
103
|
+
const schemaName = tableName.split('.')[0];
|
|
104
|
+
const result = await this.client.query(`SELECT ${schemaName}.notify_visible_messages() as count`);
|
|
105
|
+
// Track polling metrics
|
|
106
|
+
this.pollCount++;
|
|
107
|
+
const notificationCount = result.rows[0]?.count || 0;
|
|
108
|
+
this.totalNotifications += notificationCount;
|
|
109
|
+
if (notificationCount > 0) {
|
|
110
|
+
this.logger.debug('postgres-stream-router-scout-notifications', {
|
|
111
|
+
count: notificationCount,
|
|
112
|
+
totalPolls: this.pollCount,
|
|
113
|
+
totalNotifications: this.totalNotifications,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
// Log but don't throw - this is a background task
|
|
119
|
+
this.logger.debug('postgres-stream-router-scout-poll-error', {
|
|
120
|
+
error: error.message,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Reserve the router scout role using direct SQL.
|
|
126
|
+
*/
|
|
127
|
+
async reserveRouterScoutRole() {
|
|
128
|
+
try {
|
|
129
|
+
return await this.reserveScoutRole('router', enums_1.HMSH_ROUTER_SCOUT_INTERVAL_SECONDS);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
this.logger.error('postgres-stream-router-scout-reserve-error', { error });
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Reserve a scout role for the specified type.
|
|
138
|
+
* Uses SET NX (set if not exists) with expiration to ensure only one instance holds the role.
|
|
139
|
+
*/
|
|
140
|
+
async reserveScoutRole(scoutType, delay = enums_1.HMSH_ROUTER_SCOUT_INTERVAL_SECONDS) {
|
|
141
|
+
try {
|
|
142
|
+
const key = this.mintKey(key_1.KeyType.WORK_ITEMS, {
|
|
143
|
+
appId: this.appId,
|
|
144
|
+
scoutType,
|
|
145
|
+
});
|
|
146
|
+
const value = `${scoutType}:${(0, utils_1.formatISODate)(new Date())}`;
|
|
147
|
+
const expirySeconds = delay - 1;
|
|
148
|
+
const tableName = this.getTableName().split('.')[0] + '.roles';
|
|
149
|
+
// Use INSERT ... ON CONFLICT to implement SET NX with expiration
|
|
150
|
+
// Only succeeds if key doesn't exist OR if existing entry has expired
|
|
151
|
+
const result = await this.client.query(`INSERT INTO ${tableName} (key, value, expiry)
|
|
152
|
+
VALUES ($1, $2, NOW() + INTERVAL '${expirySeconds} seconds')
|
|
153
|
+
ON CONFLICT (key)
|
|
154
|
+
DO UPDATE SET
|
|
155
|
+
value = EXCLUDED.value,
|
|
156
|
+
expiry = EXCLUDED.expiry
|
|
157
|
+
WHERE ${tableName}.expiry IS NULL OR ${tableName}.expiry <= NOW()
|
|
158
|
+
RETURNING key`, [key, value]);
|
|
159
|
+
return result.rows.length > 0;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
this.logger.error('postgres-stream-reserve-scout-error', {
|
|
163
|
+
scoutType,
|
|
164
|
+
error: error.message
|
|
165
|
+
});
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Release a scout role for the specified type.
|
|
171
|
+
*/
|
|
172
|
+
async releaseScoutRole(scoutType) {
|
|
173
|
+
try {
|
|
174
|
+
const key = this.mintKey(key_1.KeyType.WORK_ITEMS, {
|
|
175
|
+
appId: this.appId,
|
|
176
|
+
scoutType,
|
|
177
|
+
});
|
|
178
|
+
const tableName = this.getTableName().split('.')[0] + '.roles';
|
|
179
|
+
const result = await this.client.query(`DELETE FROM ${tableName} WHERE key = $1`, [key]);
|
|
180
|
+
return result.rowCount > 0;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
this.logger.error('postgres-stream-release-scout-error', {
|
|
184
|
+
scoutType,
|
|
185
|
+
error: error.message
|
|
186
|
+
});
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get the router scout polling interval in milliseconds.
|
|
192
|
+
*/
|
|
193
|
+
getRouterScoutInterval() {
|
|
194
|
+
return enums_1.HMSH_ROUTER_SCOUT_INTERVAL_MS;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Check if this instance is currently the scout.
|
|
198
|
+
*/
|
|
199
|
+
isCurrentlyScout() {
|
|
200
|
+
return this.isScout;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Log polling metrics for this instance's scout tenure.
|
|
204
|
+
*/
|
|
205
|
+
logPollingMetrics() {
|
|
206
|
+
if (this.pollCount === 0) {
|
|
207
|
+
this.logger.debug('postgres-stream-router-scout-metrics', {
|
|
208
|
+
message: 'No polling occurred during this session',
|
|
209
|
+
appId: this.appId,
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const durationMs = this.scoutStartTime
|
|
214
|
+
? Date.now() - this.scoutStartTime
|
|
215
|
+
: 0;
|
|
216
|
+
const durationMinutes = durationMs / 1000 / 60;
|
|
217
|
+
const qpm = durationMinutes > 0 ? this.pollCount / durationMinutes : 0;
|
|
218
|
+
const qps = durationMs > 0 ? this.pollCount / (durationMs / 1000) : 0;
|
|
219
|
+
this.logger.info('postgres-stream-router-scout-metrics', {
|
|
220
|
+
appId: this.appId,
|
|
221
|
+
totalPolls: this.pollCount,
|
|
222
|
+
totalNotifications: this.totalNotifications,
|
|
223
|
+
durationMs: Math.round(durationMs),
|
|
224
|
+
durationMinutes: durationMinutes.toFixed(2),
|
|
225
|
+
queriesPerMinute: qpm.toFixed(2),
|
|
226
|
+
queriesPerSecond: qps.toFixed(3),
|
|
227
|
+
avgNotificationsPerPoll: this.pollCount > 0
|
|
228
|
+
? (this.totalNotifications / this.pollCount).toFixed(2)
|
|
229
|
+
: '0',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
exports.ScoutManager = ScoutManager;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ILogger } from '../../../logger';
|
|
2
|
+
import { PostgresClientType } from '../../../../types/postgres';
|
|
3
|
+
import { StreamConfig, StreamStats } from '../../../../types/stream';
|
|
4
|
+
import { ProviderClient } from '../../../../types/provider';
|
|
5
|
+
/**
|
|
6
|
+
* Get statistics for a specific stream.
|
|
7
|
+
* Returns message count and other metrics.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getStreamStats(client: PostgresClientType & ProviderClient, tableName: string, streamName: string, logger: ILogger): Promise<StreamStats>;
|
|
10
|
+
/**
|
|
11
|
+
* Get the depth (message count) for a specific stream.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getStreamDepth(client: PostgresClientType & ProviderClient, tableName: string, streamName: string, logger: ILogger): Promise<number>;
|
|
14
|
+
/**
|
|
15
|
+
* Get depths for multiple streams in a single query.
|
|
16
|
+
* More efficient than calling getStreamDepth multiple times.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getStreamDepths(client: PostgresClientType & ProviderClient, tableName: string, streamNames: {
|
|
19
|
+
stream: string;
|
|
20
|
+
}[], logger: ILogger): Promise<{
|
|
21
|
+
stream: string;
|
|
22
|
+
depth: number;
|
|
23
|
+
}[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Trim (soft delete) messages from a stream based on age or count.
|
|
26
|
+
* Returns the number of messages expired.
|
|
27
|
+
*/
|
|
28
|
+
export declare function trimStream(client: PostgresClientType & ProviderClient, tableName: string, streamName: string, options: {
|
|
29
|
+
maxLen?: number;
|
|
30
|
+
maxAge?: number;
|
|
31
|
+
exactLimit?: boolean;
|
|
32
|
+
}, logger: ILogger): Promise<number>;
|
|
33
|
+
/**
|
|
34
|
+
* Check if notifications are enabled for the provider.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isNotificationsEnabled(config?: StreamConfig): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Get provider-specific feature flags and capabilities.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getProviderSpecificFeatures(config?: StreamConfig): {
|
|
41
|
+
supportsBatching: boolean;
|
|
42
|
+
supportsDeadLetterQueue: boolean;
|
|
43
|
+
supportsOrdering: boolean;
|
|
44
|
+
supportsTrimming: boolean;
|
|
45
|
+
supportsRetry: boolean;
|
|
46
|
+
supportsNotifications: boolean;
|
|
47
|
+
maxMessageSize: number;
|
|
48
|
+
maxBatchSize: number;
|
|
49
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getProviderSpecificFeatures = exports.isNotificationsEnabled = exports.trimStream = exports.getStreamDepths = exports.getStreamDepth = exports.getStreamStats = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Get statistics for a specific stream.
|
|
6
|
+
* Returns message count and other metrics.
|
|
7
|
+
*/
|
|
8
|
+
async function getStreamStats(client, tableName, streamName, logger) {
|
|
9
|
+
try {
|
|
10
|
+
const res = await client.query(`SELECT COUNT(*) AS available_count
|
|
11
|
+
FROM ${tableName}
|
|
12
|
+
WHERE stream_name = $1 AND expired_at IS NULL`, [streamName]);
|
|
13
|
+
return {
|
|
14
|
+
messageCount: parseInt(res.rows[0].available_count, 10),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
logger.error(`postgres-stream-stats-error-${streamName}`, { error });
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.getStreamStats = getStreamStats;
|
|
23
|
+
/**
|
|
24
|
+
* Get the depth (message count) for a specific stream.
|
|
25
|
+
*/
|
|
26
|
+
async function getStreamDepth(client, tableName, streamName, logger) {
|
|
27
|
+
const stats = await getStreamStats(client, tableName, streamName, logger);
|
|
28
|
+
return stats.messageCount;
|
|
29
|
+
}
|
|
30
|
+
exports.getStreamDepth = getStreamDepth;
|
|
31
|
+
/**
|
|
32
|
+
* Get depths for multiple streams in a single query.
|
|
33
|
+
* More efficient than calling getStreamDepth multiple times.
|
|
34
|
+
*/
|
|
35
|
+
async function getStreamDepths(client, tableName, streamNames, logger) {
|
|
36
|
+
try {
|
|
37
|
+
const streams = streamNames.map((s) => s.stream);
|
|
38
|
+
const res = await client.query(`SELECT stream_name, COUNT(*) AS count
|
|
39
|
+
FROM ${tableName}
|
|
40
|
+
WHERE stream_name = ANY($1::text[])
|
|
41
|
+
GROUP BY stream_name`, [streams]);
|
|
42
|
+
const result = res.rows.map((row) => ({
|
|
43
|
+
stream: row.stream_name,
|
|
44
|
+
depth: parseInt(row.count, 10),
|
|
45
|
+
}));
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
logger.error('postgres-stream-depth-error', { error });
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.getStreamDepths = getStreamDepths;
|
|
54
|
+
/**
|
|
55
|
+
* Trim (soft delete) messages from a stream based on age or count.
|
|
56
|
+
* Returns the number of messages expired.
|
|
57
|
+
*/
|
|
58
|
+
async function trimStream(client, tableName, streamName, options, logger) {
|
|
59
|
+
try {
|
|
60
|
+
let expiredCount = 0;
|
|
61
|
+
if (options.maxLen !== undefined) {
|
|
62
|
+
const res = await client.query(`WITH to_expire AS (
|
|
63
|
+
SELECT id FROM ${tableName}
|
|
64
|
+
WHERE stream_name = $1
|
|
65
|
+
ORDER BY id ASC
|
|
66
|
+
OFFSET $2
|
|
67
|
+
)
|
|
68
|
+
UPDATE ${tableName}
|
|
69
|
+
SET expired_at = NOW()
|
|
70
|
+
WHERE id IN (SELECT id FROM to_expire)`, [streamName, options.maxLen]);
|
|
71
|
+
expiredCount += res.rowCount;
|
|
72
|
+
}
|
|
73
|
+
if (options.maxAge !== undefined) {
|
|
74
|
+
const res = await client.query(`UPDATE ${tableName}
|
|
75
|
+
SET expired_at = NOW()
|
|
76
|
+
WHERE stream_name = $1 AND created_at < NOW() - INTERVAL '${options.maxAge} milliseconds'`, [streamName]);
|
|
77
|
+
expiredCount += res.rowCount;
|
|
78
|
+
}
|
|
79
|
+
return expiredCount;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
logger.error(`postgres-stream-trim-error-${streamName}`, { error });
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.trimStream = trimStream;
|
|
87
|
+
/**
|
|
88
|
+
* Check if notifications are enabled for the provider.
|
|
89
|
+
*/
|
|
90
|
+
function isNotificationsEnabled(config = {}) {
|
|
91
|
+
// Allow override via environment variable
|
|
92
|
+
if (process.env.HOTMESH_POSTGRES_DISABLE_NOTIFICATIONS === 'true') {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return config?.postgres?.enableNotifications !== false; // Default: true
|
|
96
|
+
}
|
|
97
|
+
exports.isNotificationsEnabled = isNotificationsEnabled;
|
|
98
|
+
/**
|
|
99
|
+
* Get provider-specific feature flags and capabilities.
|
|
100
|
+
*/
|
|
101
|
+
function getProviderSpecificFeatures(config = {}) {
|
|
102
|
+
return {
|
|
103
|
+
supportsBatching: true,
|
|
104
|
+
supportsDeadLetterQueue: false,
|
|
105
|
+
supportsOrdering: true,
|
|
106
|
+
supportsTrimming: true,
|
|
107
|
+
supportsRetry: false,
|
|
108
|
+
supportsNotifications: isNotificationsEnabled(config),
|
|
109
|
+
maxMessageSize: 1024 * 1024,
|
|
110
|
+
maxBatchSize: 256,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
exports.getProviderSpecificFeatures = getProviderSpecificFeatures;
|
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SubServiceFactory = void 0;
|
|
4
4
|
const utils_1 = require("../../modules/utils");
|
|
5
|
-
const redis_1 = require("./providers/redis/redis");
|
|
6
5
|
const postgres_1 = require("./providers/postgres/postgres");
|
|
7
6
|
const nats_1 = require("./providers/nats/nats");
|
|
8
|
-
const ioredis_1 = require("./providers/redis/ioredis");
|
|
9
7
|
class SubServiceFactory {
|
|
10
8
|
static async init(providerSubClient, providerPubClient, namespace, appId, engineId, logger) {
|
|
11
9
|
let service;
|
|
@@ -13,15 +11,9 @@ class SubServiceFactory {
|
|
|
13
11
|
if (providerType === 'nats') {
|
|
14
12
|
service = new nats_1.NatsSubService(providerSubClient, providerPubClient);
|
|
15
13
|
}
|
|
16
|
-
else if (providerType === 'redis') {
|
|
17
|
-
service = new redis_1.RedisSubService(providerSubClient, providerPubClient);
|
|
18
|
-
}
|
|
19
14
|
else if (providerType === 'postgres') {
|
|
20
15
|
service = new postgres_1.PostgresSubService(providerSubClient, providerPubClient);
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
service = new ioredis_1.IORedisSubService(providerSubClient, providerPubClient);
|
|
24
|
-
}
|
|
16
|
+
} //etc
|
|
25
17
|
await service.init(namespace, appId, engineId, logger);
|
|
26
18
|
return service;
|
|
27
19
|
}
|
|
@@ -17,6 +17,6 @@ declare abstract class SubService<ClientProvider extends ProviderClient> {
|
|
|
17
17
|
abstract unsubscribe(keyType: KeyType.QUORUM, appId: string, topic?: string): Promise<void>;
|
|
18
18
|
abstract psubscribe(keyType: KeyType.QUORUM, callback: SubscriptionCallback, appId: string, topic?: string): Promise<void>;
|
|
19
19
|
abstract punsubscribe(keyType: KeyType.QUORUM, appId: string, topic?: string): Promise<void>;
|
|
20
|
-
abstract publish(keyType: KeyType, message: Record<string, any>, appId: string, topic?: string): Promise<boolean>;
|
|
20
|
+
abstract publish(keyType: KeyType, message: Record<string, any>, appId: string, topic?: string, transaction?: ProviderTransaction): Promise<boolean>;
|
|
21
21
|
}
|
|
22
22
|
export { SubService };
|
|
@@ -22,7 +22,7 @@ declare class PostgresSubService extends SubService<PostgresClientType & Provide
|
|
|
22
22
|
* Should be called when the SubService instance is being destroyed.
|
|
23
23
|
*/
|
|
24
24
|
cleanup(): Promise<void>;
|
|
25
|
-
publish(keyType: KeyType.QUORUM, message: Record<string, any>, appId: string, topic?: string): Promise<boolean>;
|
|
25
|
+
publish(keyType: KeyType.QUORUM, message: Record<string, any>, appId: string, topic?: string, transaction?: ProviderTransaction): Promise<boolean>;
|
|
26
26
|
psubscribe(): Promise<void>;
|
|
27
27
|
punsubscribe(): Promise<void>;
|
|
28
28
|
}
|
|
@@ -135,7 +135,18 @@ class PostgresSubService extends index_1.SubService {
|
|
|
135
135
|
// Stop listening to the safe topic if no more callbacks exist
|
|
136
136
|
if (callbacks.size === 0) {
|
|
137
137
|
clientSubscriptions.delete(safeKey);
|
|
138
|
-
|
|
138
|
+
// Check if client is still connected before attempting to unlisten
|
|
139
|
+
if (!this.eventClient._ending && !this.eventClient._ended) {
|
|
140
|
+
try {
|
|
141
|
+
await this.eventClient.query(`UNLISTEN "${safeKey}"`);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
// Silently handle errors if client was closed during operation
|
|
145
|
+
if (!err?.message?.includes('closed') && !err?.message?.includes('queryable')) {
|
|
146
|
+
this.logger?.error(`Error unlistening from ${safeKey}:`, err);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
139
150
|
}
|
|
140
151
|
this.logger.debug(`postgres-unsubscribe`, {
|
|
141
152
|
originalKey,
|
|
@@ -176,7 +187,7 @@ class PostgresSubService extends index_1.SubService {
|
|
|
176
187
|
PostgresSubService.clientHandlers.delete(this.eventClient);
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
|
-
async publish(keyType, message, appId, topic) {
|
|
190
|
+
async publish(keyType, message, appId, topic, transaction) {
|
|
180
191
|
const [originalKey, safeKey] = this.mintSafeKey(keyType, {
|
|
181
192
|
appId,
|
|
182
193
|
engineId: topic,
|
|
@@ -207,11 +218,47 @@ class PostgresSubService extends index_1.SubService {
|
|
|
207
218
|
jid,
|
|
208
219
|
});
|
|
209
220
|
}
|
|
210
|
-
//
|
|
221
|
+
// Escape single quotes for SQL
|
|
211
222
|
payload = payload.replace(/'/g, "''");
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
223
|
+
// If transaction provided and has addCommand (KVSQL Multi pattern),
|
|
224
|
+
// add NOTIFY to transaction
|
|
225
|
+
if (transaction &&
|
|
226
|
+
typeof transaction.addCommand === 'function') {
|
|
227
|
+
// Add NOTIFY to transaction - will execute when transaction.exec() is called
|
|
228
|
+
transaction.addCommand(`NOTIFY "${safeKey}", '${payload}'`, [], 'void');
|
|
229
|
+
this.logger.debug(`postgres-publish-transactional`, {
|
|
230
|
+
originalKey,
|
|
231
|
+
safeKey
|
|
232
|
+
});
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
// Non-transactional publish - execute immediately
|
|
236
|
+
// Check if client is still connected before attempting to publish
|
|
237
|
+
if (this.storeClient._ending || this.storeClient._ended) {
|
|
238
|
+
this.logger?.debug('postgres-publish-skipped-closed-client', { appId, topic });
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
await this.storeClient.query(`NOTIFY "${safeKey}", '${payload}'`);
|
|
243
|
+
this.logger.debug(`postgres-publish`, {
|
|
244
|
+
originalKey,
|
|
245
|
+
safeKey
|
|
246
|
+
});
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
// Handle gracefully if client was closed during operation
|
|
251
|
+
if (err?.message?.includes('closed') || err?.message?.includes('queryable')) {
|
|
252
|
+
this.logger?.debug('postgres-publish-failed-closed-client', {
|
|
253
|
+
originalKey,
|
|
254
|
+
safeKey,
|
|
255
|
+
error: err.message
|
|
256
|
+
});
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
// Re-throw other errors
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
215
262
|
}
|
|
216
263
|
async psubscribe() {
|
|
217
264
|
throw new Error('Pattern subscriptions are not supported in PostgreSQL');
|
|
@@ -14,7 +14,7 @@ declare class TaskService {
|
|
|
14
14
|
constructor(store: StoreService<ProviderClient, ProviderTransaction>, logger: ILogger);
|
|
15
15
|
processWebHooks(hookEventCallback: HookInterface): Promise<void>;
|
|
16
16
|
enqueueWorkItems(keys: string[]): Promise<void>;
|
|
17
|
-
registerJobForCleanup(jobId: string, inSeconds: number, options: JobCompletionOptions): Promise<void>;
|
|
17
|
+
registerJobForCleanup(jobId: string, inSeconds: number, options: JobCompletionOptions, transaction?: ProviderTransaction): Promise<void>;
|
|
18
18
|
registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, inSeconds: number, dad: string, transaction?: ProviderTransaction): Promise<void>;
|
|
19
19
|
/**
|
|
20
20
|
* Should this engine instance play the role of 'scout' on behalf
|
|
@@ -34,13 +34,9 @@ class TaskService {
|
|
|
34
34
|
async enqueueWorkItems(keys) {
|
|
35
35
|
await this.store.addTaskQueues(keys);
|
|
36
36
|
}
|
|
37
|
-
async registerJobForCleanup(jobId, inSeconds = enums_1.HMSH_EXPIRE_DURATION, options) {
|
|
37
|
+
async registerJobForCleanup(jobId, inSeconds = enums_1.HMSH_EXPIRE_DURATION, options, transaction) {
|
|
38
38
|
if (inSeconds > 0) {
|
|
39
|
-
await this.store.expireJob(jobId, inSeconds);
|
|
40
|
-
// const fromNow = Date.now() + inSeconds * 1000;
|
|
41
|
-
// const fidelityMS = HMSH_FIDELITY_SECONDS * 1000;
|
|
42
|
-
// const timeSlot = Math.floor(fromNow / fidelityMS) * fidelityMS;
|
|
43
|
-
// await this.store.registerDependenciesForCleanup(jobId, timeSlot, options);
|
|
39
|
+
await this.store.expireJob(jobId, inSeconds, transaction);
|
|
44
40
|
}
|
|
45
41
|
}
|
|
46
42
|
async registerTimeHook(jobId, gId, activityId, type, inSeconds = enums_1.HMSH_FIDELITY_SECONDS, dad, transaction) {
|
|
@@ -38,6 +38,7 @@ class WorkerService {
|
|
|
38
38
|
service.topic = worker.topic;
|
|
39
39
|
service.config = config;
|
|
40
40
|
service.logger = logger;
|
|
41
|
+
service.retryPolicy = worker.retryPolicy;
|
|
41
42
|
await service.initStoreChannel(service, worker.store);
|
|
42
43
|
await service.initSubChannel(service, worker.sub, worker.pub ?? worker.store);
|
|
43
44
|
await service.subscribe.subscribe(key_1.KeyType.QUORUM, service.subscriptionHandler(), appId);
|
|
@@ -99,6 +100,7 @@ class WorkerService {
|
|
|
99
100
|
reclaimDelay: worker.reclaimDelay,
|
|
100
101
|
reclaimCount: worker.reclaimCount,
|
|
101
102
|
throttle,
|
|
103
|
+
retryPolicy: worker.retryPolicy,
|
|
102
104
|
}, this.stream, logger);
|
|
103
105
|
}
|
|
104
106
|
/**
|