@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.
Files changed (112) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/README.md +179 -142
  3. package/build/index.d.ts +1 -3
  4. package/build/index.js +1 -5
  5. package/build/modules/enums.d.ts +7 -0
  6. package/build/modules/enums.js +16 -1
  7. package/build/modules/utils.d.ts +27 -0
  8. package/build/modules/utils.js +55 -32
  9. package/build/package.json +18 -27
  10. package/build/services/activities/activity.d.ts +43 -6
  11. package/build/services/activities/activity.js +262 -54
  12. package/build/services/activities/await.js +2 -2
  13. package/build/services/activities/cycle.js +1 -1
  14. package/build/services/activities/hook.d.ts +5 -0
  15. package/build/services/activities/hook.js +22 -19
  16. package/build/services/activities/interrupt.js +17 -25
  17. package/build/services/activities/signal.d.ts +4 -2
  18. package/build/services/activities/signal.js +27 -24
  19. package/build/services/activities/worker.js +2 -2
  20. package/build/services/collator/index.d.ts +123 -25
  21. package/build/services/collator/index.js +224 -101
  22. package/build/services/connector/factory.d.ts +1 -1
  23. package/build/services/connector/factory.js +1 -11
  24. package/build/services/connector/providers/postgres.js +3 -0
  25. package/build/services/engine/index.d.ts +5 -5
  26. package/build/services/engine/index.js +36 -15
  27. package/build/services/hotmesh/index.d.ts +66 -15
  28. package/build/services/hotmesh/index.js +84 -15
  29. package/build/services/memflow/index.d.ts +100 -14
  30. package/build/services/memflow/index.js +100 -14
  31. package/build/services/memflow/worker.d.ts +97 -0
  32. package/build/services/memflow/worker.js +217 -0
  33. package/build/services/memflow/workflow/proxyActivities.d.ts +74 -3
  34. package/build/services/memflow/workflow/proxyActivities.js +81 -4
  35. package/build/services/router/consumption/index.d.ts +2 -1
  36. package/build/services/router/consumption/index.js +39 -3
  37. package/build/services/router/error-handling/index.d.ts +3 -3
  38. package/build/services/router/error-handling/index.js +48 -13
  39. package/build/services/router/index.d.ts +1 -0
  40. package/build/services/router/index.js +2 -1
  41. package/build/services/search/factory.js +1 -9
  42. package/build/services/store/factory.js +1 -9
  43. package/build/services/store/index.d.ts +8 -2
  44. package/build/services/store/providers/postgres/kvsql.d.ts +4 -0
  45. package/build/services/store/providers/postgres/kvsql.js +4 -0
  46. package/build/services/store/providers/postgres/kvtransaction.d.ts +2 -0
  47. package/build/services/store/providers/postgres/kvtransaction.js +23 -0
  48. package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +51 -0
  49. package/build/services/store/providers/postgres/kvtypes/hash/basic.js +229 -7
  50. package/build/services/store/providers/postgres/kvtypes/hash/expire.js +12 -2
  51. package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +4 -0
  52. package/build/services/store/providers/postgres/kvtypes/hash/index.js +6 -0
  53. package/build/services/store/providers/postgres/kvtypes/hash/scan.js +30 -10
  54. package/build/services/store/providers/postgres/kvtypes/list.js +68 -10
  55. package/build/services/store/providers/postgres/kvtypes/string.js +60 -10
  56. package/build/services/store/providers/postgres/kvtypes/zset.js +92 -22
  57. package/build/services/store/providers/postgres/postgres.d.ts +23 -3
  58. package/build/services/store/providers/postgres/postgres.js +38 -1
  59. package/build/services/stream/factory.js +1 -17
  60. package/build/services/stream/providers/postgres/kvtables.js +76 -23
  61. package/build/services/stream/providers/postgres/lifecycle.d.ts +19 -0
  62. package/build/services/stream/providers/postgres/lifecycle.js +54 -0
  63. package/build/services/stream/providers/postgres/messages.d.ts +56 -0
  64. package/build/services/stream/providers/postgres/messages.js +253 -0
  65. package/build/services/stream/providers/postgres/notifications.d.ts +59 -0
  66. package/build/services/stream/providers/postgres/notifications.js +357 -0
  67. package/build/services/stream/providers/postgres/postgres.d.ts +110 -11
  68. package/build/services/stream/providers/postgres/postgres.js +196 -488
  69. package/build/services/stream/providers/postgres/scout.d.ts +68 -0
  70. package/build/services/stream/providers/postgres/scout.js +233 -0
  71. package/build/services/stream/providers/postgres/stats.d.ts +49 -0
  72. package/build/services/stream/providers/postgres/stats.js +113 -0
  73. package/build/services/sub/factory.js +1 -9
  74. package/build/services/sub/index.d.ts +1 -1
  75. package/build/services/sub/providers/postgres/postgres.d.ts +1 -1
  76. package/build/services/sub/providers/postgres/postgres.js +53 -6
  77. package/build/services/task/index.d.ts +1 -1
  78. package/build/services/task/index.js +2 -6
  79. package/build/services/worker/index.d.ts +1 -0
  80. package/build/services/worker/index.js +2 -0
  81. package/build/types/hotmesh.d.ts +42 -2
  82. package/build/types/index.d.ts +3 -4
  83. package/build/types/index.js +1 -4
  84. package/build/types/memflow.d.ts +32 -0
  85. package/build/types/provider.d.ts +17 -1
  86. package/build/types/stream.d.ts +92 -1
  87. package/index.ts +0 -4
  88. package/package.json +18 -27
  89. package/build/services/connector/providers/ioredis.d.ts +0 -9
  90. package/build/services/connector/providers/ioredis.js +0 -26
  91. package/build/services/connector/providers/redis.d.ts +0 -9
  92. package/build/services/connector/providers/redis.js +0 -38
  93. package/build/services/search/providers/redis/ioredis.d.ts +0 -23
  94. package/build/services/search/providers/redis/ioredis.js +0 -189
  95. package/build/services/search/providers/redis/redis.d.ts +0 -23
  96. package/build/services/search/providers/redis/redis.js +0 -202
  97. package/build/services/store/providers/redis/_base.d.ts +0 -137
  98. package/build/services/store/providers/redis/_base.js +0 -980
  99. package/build/services/store/providers/redis/ioredis.d.ts +0 -20
  100. package/build/services/store/providers/redis/ioredis.js +0 -180
  101. package/build/services/store/providers/redis/redis.d.ts +0 -18
  102. package/build/services/store/providers/redis/redis.js +0 -199
  103. package/build/services/stream/providers/redis/ioredis.d.ts +0 -61
  104. package/build/services/stream/providers/redis/ioredis.js +0 -272
  105. package/build/services/stream/providers/redis/redis.d.ts +0 -61
  106. package/build/services/stream/providers/redis/redis.js +0 -305
  107. package/build/services/sub/providers/redis/ioredis.d.ts +0 -20
  108. package/build/services/sub/providers/redis/ioredis.js +0 -150
  109. package/build/services/sub/providers/redis/redis.d.ts +0 -18
  110. package/build/services/sub/providers/redis/redis.js +0 -137
  111. package/build/types/redis.d.ts +0 -258
  112. 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
- await this.eventClient.query(`UNLISTEN "${safeKey}"`);
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
- // Publish the message using the safe topic
221
+ // Escape single quotes for SQL
211
222
  payload = payload.replace(/'/g, "''");
212
- await this.storeClient.query(`NOTIFY "${safeKey}", '${payload}'`);
213
- this.logger.debug(`postgres-publish`, { originalKey, safeKey });
214
- return true;
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) {
@@ -23,6 +23,7 @@ declare class WorkerService {
23
23
  reporting: boolean;
24
24
  inited: string;
25
25
  rollCallInterval: NodeJS.Timeout;
26
+ retryPolicy: import('../../types/stream').RetryPolicy | undefined;
26
27
  /**
27
28
  * @private
28
29
  */
@@ -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
  /**