@hotmeshio/hotmesh 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -14
- package/build/package.json +1 -1
- package/build/services/memflow/workflow/signal.d.ts +23 -1
- package/build/services/memflow/workflow/signal.js +23 -1
- package/build/services/memflow/workflow/sleepFor.d.ts +17 -1
- package/build/services/memflow/workflow/sleepFor.js +17 -1
- package/build/services/memflow/workflow/waitFor.d.ts +22 -1
- package/build/services/memflow/workflow/waitFor.js +22 -1
- package/build/services/store/providers/postgres/postgres.d.ts +33 -0
- package/build/services/store/providers/postgres/postgres.js +208 -0
- package/build/services/task/index.d.ts +12 -0
- package/build/services/task/index.js +47 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
# HotMesh
|
|
1
|
+
# HotMesh
|
|
2
2
|
|
|
3
3
|
**Permanent-Memory Workflows & AI Agents**
|
|
4
4
|
|
|
5
5
|
 
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
**HotMesh** is a Temporal-style workflow engine that runs natively on PostgreSQL — with a powerful twist: every workflow maintains a permanent, JSON-backed context that persists independently of the workflow itself.
|
|
8
|
+
|
|
9
|
+
This means:
|
|
10
|
+
|
|
11
|
+
* Any number of lightweight, thread-safe **hook workers** can attach to the same workflow record at any time.
|
|
12
|
+
* These hooks can safely **read and incrementally write** to shared state.
|
|
13
|
+
* The result is a **durable execution model** with **evolving memory**, ideal for **human-in-the-loop processes** and **AI agents that learn over time**.
|
|
13
14
|
|
|
14
15
|
---
|
|
15
16
|
|
|
@@ -61,10 +62,11 @@ async function main() {
|
|
|
61
62
|
main().catch(console.error);
|
|
62
63
|
```
|
|
63
64
|
|
|
65
|
+
---
|
|
64
66
|
|
|
65
67
|
## 🧠 How Permanent Memory Works
|
|
66
68
|
|
|
67
|
-
* **Context = JSONB row
|
|
69
|
+
* **Context = persistent JSON record** – each workflow's memory is stored as a JSONB row in your Postgres database
|
|
68
70
|
* **Atomic operations** (`set`, `merge`, `append`, `increment`, `toggle`, `delete`, …)
|
|
69
71
|
* **Transactional** – every update participates in the workflow/DB transaction
|
|
70
72
|
* **Time-travel-safe** – full replay compatibility; side-effect detector guarantees determinism
|
|
@@ -135,14 +137,37 @@ export async function hook2(name: string, kind: string): Promise<void> {
|
|
|
135
137
|
await ctx.merge({ user: { lastSeen: new Date().toISOString() } });
|
|
136
138
|
await MemFlow.workflow.signal('hook2-complete', { ok: true });
|
|
137
139
|
}
|
|
138
|
-
```
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
/* ------------ Worker/Hook Registration ------------ */
|
|
142
|
+
async function startWorker() {
|
|
143
|
+
const mf = await MemFlow.init({
|
|
144
|
+
appId: 'my-app',
|
|
145
|
+
engine: {
|
|
146
|
+
connection: {
|
|
147
|
+
class: Postgres,
|
|
148
|
+
options: { connectionString: process.env.DATABASE_URL }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const worker = await mf.worker.create({
|
|
154
|
+
taskQueue: 'contextual',
|
|
155
|
+
workflow: example
|
|
156
|
+
});
|
|
141
157
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
await mf.worker.create({
|
|
159
|
+
taskQueue: 'contextual',
|
|
160
|
+
workflow: hook1
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await mf.worker.create({
|
|
164
|
+
taskQueue: 'contextual',
|
|
165
|
+
workflow: hook2
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
console.log('Workers and hooks started and listening...');
|
|
169
|
+
}
|
|
170
|
+
```
|
|
146
171
|
|
|
147
172
|
---
|
|
148
173
|
|
package/build/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sends a signal payload to any paused workflow thread awaiting this signal.
|
|
3
|
-
*
|
|
3
|
+
* This method is commonly used to coordinate between workflows, hook functions,
|
|
4
|
+
* and external events.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* // Basic usage - send a simple signal with data
|
|
8
|
+
* await MemFlow.workflow.signal('signal-id', { name: 'WarmMash' });
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Hook function signaling completion
|
|
12
|
+
* export async function exampleHook(name: string): Promise<void> {
|
|
13
|
+
* const result = await processData(name);
|
|
14
|
+
* await MemFlow.workflow.signal('hook-complete', { data: result });
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Signal with complex data structure
|
|
19
|
+
* await MemFlow.workflow.signal('process-complete', {
|
|
20
|
+
* status: 'success',
|
|
21
|
+
* data: { id: 123, name: 'test' },
|
|
22
|
+
* timestamp: new Date().toISOString()
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* @param {string} signalId - Unique signal identifier that matches a waitFor() call.
|
|
4
26
|
* @param {Record<any, any>} data - The payload to send with the signal.
|
|
5
27
|
* @returns {Promise<string>} The resulting hook/stream ID.
|
|
6
28
|
*/
|
|
@@ -5,7 +5,29 @@ const common_1 = require("./common");
|
|
|
5
5
|
const isSideEffectAllowed_1 = require("./isSideEffectAllowed");
|
|
6
6
|
/**
|
|
7
7
|
* Sends a signal payload to any paused workflow thread awaiting this signal.
|
|
8
|
-
*
|
|
8
|
+
* This method is commonly used to coordinate between workflows, hook functions,
|
|
9
|
+
* and external events.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // Basic usage - send a simple signal with data
|
|
13
|
+
* await MemFlow.workflow.signal('signal-id', { name: 'WarmMash' });
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Hook function signaling completion
|
|
17
|
+
* export async function exampleHook(name: string): Promise<void> {
|
|
18
|
+
* const result = await processData(name);
|
|
19
|
+
* await MemFlow.workflow.signal('hook-complete', { data: result });
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Signal with complex data structure
|
|
24
|
+
* await MemFlow.workflow.signal('process-complete', {
|
|
25
|
+
* status: 'success',
|
|
26
|
+
* data: { id: 123, name: 'test' },
|
|
27
|
+
* timestamp: new Date().toISOString()
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* @param {string} signalId - Unique signal identifier that matches a waitFor() call.
|
|
9
31
|
* @param {Record<any, any>} data - The payload to send with the signal.
|
|
10
32
|
* @returns {Promise<string>} The resulting hook/stream ID.
|
|
11
33
|
*/
|
|
@@ -2,7 +2,23 @@
|
|
|
2
2
|
* Sleeps the workflow for a specified duration, deterministically.
|
|
3
3
|
* On replay, it will not actually sleep again, but resume after sleep.
|
|
4
4
|
*
|
|
5
|
-
* @
|
|
5
|
+
* @example
|
|
6
|
+
* // Basic usage - sleep for a specific duration
|
|
7
|
+
* await MemFlow.workflow.sleepFor('2 seconds');
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Using with Promise.all for parallel operations
|
|
11
|
+
* const [greeting, timeInSeconds] = await Promise.all([
|
|
12
|
+
* someActivity(name),
|
|
13
|
+
* MemFlow.workflow.sleepFor('1 second')
|
|
14
|
+
* ]);
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Multiple sequential sleeps
|
|
18
|
+
* await MemFlow.workflow.sleepFor('1 seconds'); // First pause
|
|
19
|
+
* await MemFlow.workflow.sleepFor('2 seconds'); // Second pause
|
|
20
|
+
*
|
|
21
|
+
* @param {string} duration - A human-readable duration string (e.g., '1m', '2 hours', '30 seconds').
|
|
6
22
|
* @returns {Promise<number>} The resolved duration in seconds.
|
|
7
23
|
*/
|
|
8
24
|
export declare function sleepFor(duration: string): Promise<number>;
|
|
@@ -7,7 +7,23 @@ const didRun_1 = require("./didRun");
|
|
|
7
7
|
* Sleeps the workflow for a specified duration, deterministically.
|
|
8
8
|
* On replay, it will not actually sleep again, but resume after sleep.
|
|
9
9
|
*
|
|
10
|
-
* @
|
|
10
|
+
* @example
|
|
11
|
+
* // Basic usage - sleep for a specific duration
|
|
12
|
+
* await MemFlow.workflow.sleepFor('2 seconds');
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Using with Promise.all for parallel operations
|
|
16
|
+
* const [greeting, timeInSeconds] = await Promise.all([
|
|
17
|
+
* someActivity(name),
|
|
18
|
+
* MemFlow.workflow.sleepFor('1 second')
|
|
19
|
+
* ]);
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // Multiple sequential sleeps
|
|
23
|
+
* await MemFlow.workflow.sleepFor('1 seconds'); // First pause
|
|
24
|
+
* await MemFlow.workflow.sleepFor('2 seconds'); // Second pause
|
|
25
|
+
*
|
|
26
|
+
* @param {string} duration - A human-readable duration string (e.g., '1m', '2 hours', '30 seconds').
|
|
11
27
|
* @returns {Promise<number>} The resolved duration in seconds.
|
|
12
28
|
*/
|
|
13
29
|
async function sleepFor(duration) {
|
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pauses the workflow until a signal with the given `signalId` is received.
|
|
3
|
+
* This method is commonly used to coordinate between the main workflow and hook functions,
|
|
4
|
+
* or to wait for external events.
|
|
3
5
|
*
|
|
4
|
-
* @
|
|
6
|
+
* @example
|
|
7
|
+
* // Basic usage - wait for a single signal
|
|
8
|
+
* const payload = await MemFlow.workflow.waitFor<PayloadType>('abcdefg');
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Wait for multiple signals in parallel
|
|
12
|
+
* const [signal1, signal2] = await Promise.all([
|
|
13
|
+
* MemFlow.workflow.waitFor<Record<string, any>>('signal1'),
|
|
14
|
+
* MemFlow.workflow.waitFor<Record<string, any>>('signal2')
|
|
15
|
+
* ]);
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Typical pattern with hook functions
|
|
19
|
+
* // In main workflow:
|
|
20
|
+
* await MemFlow.workflow.waitFor<ResponseType>('hook-complete');
|
|
21
|
+
*
|
|
22
|
+
* // In hook function:
|
|
23
|
+
* await MemFlow.workflow.signal('hook-complete', { data: result });
|
|
24
|
+
*
|
|
25
|
+
* @template T - The type of data expected in the signal payload
|
|
5
26
|
* @param {string} signalId - A unique signal identifier shared by the sender and receiver.
|
|
6
27
|
* @returns {Promise<T>} The data payload associated with the received signal.
|
|
7
28
|
*/
|
|
@@ -5,8 +5,29 @@ const common_1 = require("./common");
|
|
|
5
5
|
const didRun_1 = require("./didRun");
|
|
6
6
|
/**
|
|
7
7
|
* Pauses the workflow until a signal with the given `signalId` is received.
|
|
8
|
+
* This method is commonly used to coordinate between the main workflow and hook functions,
|
|
9
|
+
* or to wait for external events.
|
|
8
10
|
*
|
|
9
|
-
* @
|
|
11
|
+
* @example
|
|
12
|
+
* // Basic usage - wait for a single signal
|
|
13
|
+
* const payload = await MemFlow.workflow.waitFor<PayloadType>('abcdefg');
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Wait for multiple signals in parallel
|
|
17
|
+
* const [signal1, signal2] = await Promise.all([
|
|
18
|
+
* MemFlow.workflow.waitFor<Record<string, any>>('signal1'),
|
|
19
|
+
* MemFlow.workflow.waitFor<Record<string, any>>('signal2')
|
|
20
|
+
* ]);
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Typical pattern with hook functions
|
|
24
|
+
* // In main workflow:
|
|
25
|
+
* await MemFlow.workflow.waitFor<ResponseType>('hook-complete');
|
|
26
|
+
*
|
|
27
|
+
* // In hook function:
|
|
28
|
+
* await MemFlow.workflow.signal('hook-complete', { data: result });
|
|
29
|
+
*
|
|
30
|
+
* @template T - The type of data expected in the signal payload
|
|
10
31
|
* @param {string} signalId - A unique signal identifier shared by the sender and receiver.
|
|
11
32
|
* @returns {Promise<T>} The data payload associated with the received signal.
|
|
12
33
|
*/
|
|
@@ -18,6 +18,7 @@ import { KVTables } from './kvtables';
|
|
|
18
18
|
declare class PostgresStoreService extends StoreService<ProviderClient, ProviderTransaction> {
|
|
19
19
|
pgClient: PostgresClientType;
|
|
20
20
|
kvTables: ReturnType<typeof KVTables>;
|
|
21
|
+
isScout: boolean;
|
|
21
22
|
transact(): ProviderTransaction;
|
|
22
23
|
constructor(storeClient: ProviderClient);
|
|
23
24
|
init(namespace: string, appId: string, logger: ILogger): Promise<HotMeshApps>;
|
|
@@ -141,5 +142,37 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
|
|
|
141
142
|
setThrottleRate(options: ThrottleOptions): Promise<void>;
|
|
142
143
|
getThrottleRates(): Promise<StringStringType>;
|
|
143
144
|
getThrottleRate(topic: string): Promise<number>;
|
|
145
|
+
/**
|
|
146
|
+
* Deploy time-aware notification triggers and functions
|
|
147
|
+
*/
|
|
148
|
+
private deployTimeNotificationTriggers;
|
|
149
|
+
/**
|
|
150
|
+
* Enhanced time scout that uses LISTEN/NOTIFY to reduce polling
|
|
151
|
+
*/
|
|
152
|
+
startTimeScoutWithNotifications(timeEventCallback: (jobId: string, gId: string, activityId: string, type: WorkListTaskType) => Promise<void>): Promise<void>;
|
|
153
|
+
/**
|
|
154
|
+
* Handle time notifications from PostgreSQL
|
|
155
|
+
*/
|
|
156
|
+
private handleTimeNotification;
|
|
157
|
+
/**
|
|
158
|
+
* Process time hooks that are ready to be awakened
|
|
159
|
+
*/
|
|
160
|
+
private processReadyTimeHooks;
|
|
161
|
+
/**
|
|
162
|
+
* Enhanced time scout process that uses notifications
|
|
163
|
+
*/
|
|
164
|
+
private processTimeHooksWithNotifications;
|
|
165
|
+
/**
|
|
166
|
+
* Get the next awakening time from the database
|
|
167
|
+
*/
|
|
168
|
+
private getNextAwakeningTime;
|
|
169
|
+
/**
|
|
170
|
+
* Update the time scout's sleep timing based on schedule changes
|
|
171
|
+
*/
|
|
172
|
+
private updateTimeScoutSleep;
|
|
173
|
+
/**
|
|
174
|
+
* Enhanced shouldScout that can handle notifications
|
|
175
|
+
*/
|
|
176
|
+
shouldScout(): Promise<boolean>;
|
|
144
177
|
}
|
|
145
178
|
export { PostgresStoreService };
|
|
@@ -39,6 +39,7 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
39
39
|
}
|
|
40
40
|
constructor(storeClient) {
|
|
41
41
|
super(storeClient);
|
|
42
|
+
this.isScout = false;
|
|
42
43
|
//Instead of directly referencing the 'pg' package and methods like 'query',
|
|
43
44
|
// the PostgresStore wraps the 'pg' client in a class that implements
|
|
44
45
|
// the Redis client interface. This allows the same methods to be called
|
|
@@ -58,6 +59,8 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
58
59
|
this.logger = logger;
|
|
59
60
|
//confirm db tables exist
|
|
60
61
|
await this.kvTables.deploy(appId);
|
|
62
|
+
// Deploy time notification triggers
|
|
63
|
+
await this.deployTimeNotificationTriggers(appId);
|
|
61
64
|
//note: getSettings will contact db to confirm r/w access
|
|
62
65
|
const settings = await this.getSettings(true);
|
|
63
66
|
this.cache = new cache_1.Cache(appId, settings);
|
|
@@ -1032,5 +1035,210 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1032
1035
|
}
|
|
1033
1036
|
return resolveRate(response, topic);
|
|
1034
1037
|
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Deploy time-aware notification triggers and functions
|
|
1040
|
+
*/
|
|
1041
|
+
async deployTimeNotificationTriggers(appId) {
|
|
1042
|
+
const schemaName = this.kvsql().safeName(appId);
|
|
1043
|
+
const client = this.pgClient;
|
|
1044
|
+
try {
|
|
1045
|
+
// Read the SQL template and replace schema placeholder
|
|
1046
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1047
|
+
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1048
|
+
const sqlTemplate = fs.readFileSync(path.join(__dirname, 'time-notify.sql'), 'utf8');
|
|
1049
|
+
const sql = sqlTemplate.replace(/{schema}/g, schemaName);
|
|
1050
|
+
// Execute the entire SQL as one statement (functions contain $$ blocks with semicolons)
|
|
1051
|
+
await client.query(sql);
|
|
1052
|
+
this.logger.info('postgres-time-notifications-deployed', {
|
|
1053
|
+
appId,
|
|
1054
|
+
schemaName,
|
|
1055
|
+
message: 'Time-aware notifications ENABLED - using LISTEN/NOTIFY instead of polling'
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
catch (error) {
|
|
1059
|
+
this.logger.error('postgres-time-notifications-deploy-error', {
|
|
1060
|
+
appId,
|
|
1061
|
+
schemaName,
|
|
1062
|
+
error: error.message
|
|
1063
|
+
});
|
|
1064
|
+
// Don't throw - fall back to polling mode
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Enhanced time scout that uses LISTEN/NOTIFY to reduce polling
|
|
1069
|
+
*/
|
|
1070
|
+
async startTimeScoutWithNotifications(timeEventCallback) {
|
|
1071
|
+
const channelName = `time_hooks_${this.appId}`;
|
|
1072
|
+
try {
|
|
1073
|
+
// Set up LISTEN for time notifications
|
|
1074
|
+
await this.pgClient.query(`LISTEN "${channelName}"`);
|
|
1075
|
+
// Set up notification handler
|
|
1076
|
+
this.pgClient.on('notification', (notification) => {
|
|
1077
|
+
this.handleTimeNotification(notification, timeEventCallback);
|
|
1078
|
+
});
|
|
1079
|
+
this.logger.debug('postgres-time-scout-notifications-started', {
|
|
1080
|
+
appId: this.appId,
|
|
1081
|
+
channelName
|
|
1082
|
+
});
|
|
1083
|
+
// Start the enhanced time scout loop
|
|
1084
|
+
await this.processTimeHooksWithNotifications(timeEventCallback);
|
|
1085
|
+
}
|
|
1086
|
+
catch (error) {
|
|
1087
|
+
this.logger.error('postgres-time-scout-notifications-error', {
|
|
1088
|
+
appId: this.appId,
|
|
1089
|
+
error
|
|
1090
|
+
});
|
|
1091
|
+
// Fall back to regular polling mode
|
|
1092
|
+
throw error;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Handle time notifications from PostgreSQL
|
|
1097
|
+
*/
|
|
1098
|
+
async handleTimeNotification(notification, timeEventCallback) {
|
|
1099
|
+
try {
|
|
1100
|
+
const payload = JSON.parse(notification.payload);
|
|
1101
|
+
const { type, app_id, next_awakening, ready_at } = payload;
|
|
1102
|
+
if (app_id !== this.appId) {
|
|
1103
|
+
return; // Not for this app
|
|
1104
|
+
}
|
|
1105
|
+
this.logger.debug('postgres-time-notification-received', {
|
|
1106
|
+
type,
|
|
1107
|
+
appId: app_id,
|
|
1108
|
+
nextAwakening: next_awakening,
|
|
1109
|
+
readyAt: ready_at
|
|
1110
|
+
});
|
|
1111
|
+
if (type === 'time_hooks_ready') {
|
|
1112
|
+
// Process any ready time hooks immediately
|
|
1113
|
+
await this.processReadyTimeHooks(timeEventCallback);
|
|
1114
|
+
}
|
|
1115
|
+
else if (type === 'time_schedule_updated') {
|
|
1116
|
+
// Update our sleep timing if we're the time scout
|
|
1117
|
+
await this.updateTimeScoutSleep(next_awakening);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
catch (error) {
|
|
1121
|
+
this.logger.error('postgres-time-notification-handle-error', {
|
|
1122
|
+
notification,
|
|
1123
|
+
error
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Process time hooks that are ready to be awakened
|
|
1129
|
+
*/
|
|
1130
|
+
async processReadyTimeHooks(timeEventCallback) {
|
|
1131
|
+
let hasMoreTasks = true;
|
|
1132
|
+
while (hasMoreTasks) {
|
|
1133
|
+
const workListTask = await this.getNextTask();
|
|
1134
|
+
if (Array.isArray(workListTask)) {
|
|
1135
|
+
const [listKey, target, gId, activityId, type] = workListTask;
|
|
1136
|
+
if (type === 'child') {
|
|
1137
|
+
// Skip child tasks - they're handled by ancestors
|
|
1138
|
+
}
|
|
1139
|
+
else if (type === 'delist') {
|
|
1140
|
+
// Delist the signal key
|
|
1141
|
+
const key = this.mintKey(key_1.KeyType.SIGNALS, { appId: this.appId });
|
|
1142
|
+
await this.delistSignalKey(key, target);
|
|
1143
|
+
}
|
|
1144
|
+
else {
|
|
1145
|
+
// Process the task
|
|
1146
|
+
await timeEventCallback(target, gId, activityId, type);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
else if (workListTask === true) {
|
|
1150
|
+
// A worklist was emptied, continue processing
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
else {
|
|
1154
|
+
// No more tasks ready
|
|
1155
|
+
hasMoreTasks = false;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Enhanced time scout process that uses notifications
|
|
1161
|
+
*/
|
|
1162
|
+
async processTimeHooksWithNotifications(timeEventCallback) {
|
|
1163
|
+
let currentSleepTimeout = null;
|
|
1164
|
+
// Function to calculate next sleep duration
|
|
1165
|
+
const calculateNextSleep = async () => {
|
|
1166
|
+
const nextAwakeningTime = await this.getNextAwakeningTime();
|
|
1167
|
+
if (nextAwakeningTime) {
|
|
1168
|
+
const sleepMs = nextAwakeningTime - Date.now();
|
|
1169
|
+
return Math.max(sleepMs, 0);
|
|
1170
|
+
}
|
|
1171
|
+
// Default sleep if no tasks scheduled
|
|
1172
|
+
return enums_1.HMSH_FIDELITY_SECONDS * 1000;
|
|
1173
|
+
};
|
|
1174
|
+
// Main loop
|
|
1175
|
+
while (true) {
|
|
1176
|
+
try {
|
|
1177
|
+
if (await this.shouldScout()) {
|
|
1178
|
+
// Process any ready tasks
|
|
1179
|
+
await this.processReadyTimeHooks(timeEventCallback);
|
|
1180
|
+
// Calculate next sleep
|
|
1181
|
+
const sleepMs = await calculateNextSleep();
|
|
1182
|
+
// Sleep with ability to be interrupted by notifications
|
|
1183
|
+
await new Promise((resolve) => {
|
|
1184
|
+
currentSleepTimeout = setTimeout(resolve, sleepMs);
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
// Not the scout, sleep longer
|
|
1189
|
+
await (0, utils_1.sleepFor)(enums_1.HMSH_SCOUT_INTERVAL_SECONDS * 1000);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
catch (error) {
|
|
1193
|
+
this.logger.error('postgres-time-scout-loop-error', { error });
|
|
1194
|
+
await (0, utils_1.sleepFor)(1000);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Get the next awakening time from the database
|
|
1200
|
+
*/
|
|
1201
|
+
async getNextAwakeningTime() {
|
|
1202
|
+
const schemaName = this.kvsql().safeName(this.appId);
|
|
1203
|
+
const appKey = `${this.appId}:time_range`;
|
|
1204
|
+
try {
|
|
1205
|
+
const result = await this.pgClient.query(`SELECT ${schemaName}.get_next_awakening_time($1) as next_time`, [appKey]);
|
|
1206
|
+
if (result.rows[0]?.next_time) {
|
|
1207
|
+
return new Date(result.rows[0].next_time).getTime();
|
|
1208
|
+
}
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
catch (error) {
|
|
1212
|
+
this.logger.error('postgres-get-next-awakening-error', { error });
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Update the time scout's sleep timing based on schedule changes
|
|
1218
|
+
*/
|
|
1219
|
+
async updateTimeScoutSleep(nextAwakening) {
|
|
1220
|
+
// This could be used to interrupt current sleep and recalculate
|
|
1221
|
+
// For now, just log the schedule update
|
|
1222
|
+
this.logger.debug('postgres-time-schedule-updated', {
|
|
1223
|
+
nextAwakening,
|
|
1224
|
+
currentTime: Date.now()
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Enhanced shouldScout that can handle notifications
|
|
1229
|
+
*/
|
|
1230
|
+
async shouldScout() {
|
|
1231
|
+
const wasScout = this.isScout;
|
|
1232
|
+
const isScout = wasScout || (this.isScout = await this.reserveScoutRole('time'));
|
|
1233
|
+
if (isScout) {
|
|
1234
|
+
if (!wasScout) {
|
|
1235
|
+
setTimeout(() => {
|
|
1236
|
+
this.isScout = false;
|
|
1237
|
+
}, enums_1.HMSH_SCOUT_INTERVAL_SECONDS * 1000);
|
|
1238
|
+
}
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1035
1243
|
}
|
|
1036
1244
|
exports.PostgresStoreService = PostgresStoreService;
|
|
@@ -32,5 +32,17 @@ declare class TaskService {
|
|
|
32
32
|
registerWebHook(topic: string, context: JobState, dad: string, expire: number, transaction?: ProviderTransaction): Promise<string>;
|
|
33
33
|
processWebHookSignal(topic: string, data: Record<string, unknown>): Promise<[string, string, string, string] | undefined>;
|
|
34
34
|
deleteWebHookSignal(topic: string, data: Record<string, unknown>): Promise<number>;
|
|
35
|
+
/**
|
|
36
|
+
* Enhanced processTimeHooks that uses notifications for PostgreSQL stores
|
|
37
|
+
*/
|
|
38
|
+
processTimeHooksWithNotifications(timeEventCallback: (jobId: string, gId: string, activityId: string, type: WorkListTaskType) => Promise<void>): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Check if this is a PostgreSQL store
|
|
41
|
+
*/
|
|
42
|
+
private isPostgresStore;
|
|
43
|
+
/**
|
|
44
|
+
* Check if the store supports notifications
|
|
45
|
+
*/
|
|
46
|
+
private supportsNotifications;
|
|
35
47
|
}
|
|
36
48
|
export { TaskService };
|
|
@@ -202,5 +202,52 @@ class TaskService {
|
|
|
202
202
|
throw new Error('signaler.process:error: hook rule not found');
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Enhanced processTimeHooks that uses notifications for PostgreSQL stores
|
|
207
|
+
*/
|
|
208
|
+
async processTimeHooksWithNotifications(timeEventCallback) {
|
|
209
|
+
// Check if the store supports notifications
|
|
210
|
+
if (this.isPostgresStore() && this.supportsNotifications()) {
|
|
211
|
+
try {
|
|
212
|
+
this.logger.info('task-using-notification-mode', {
|
|
213
|
+
appId: this.store.appId,
|
|
214
|
+
message: 'Time scout using PostgreSQL LISTEN/NOTIFY mode for efficient task processing'
|
|
215
|
+
});
|
|
216
|
+
// Use the PostgreSQL store's notification-based approach
|
|
217
|
+
await this.store.startTimeScoutWithNotifications(timeEventCallback);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
this.logger.warn('task-notifications-fallback', {
|
|
221
|
+
appId: this.store.appId,
|
|
222
|
+
error: error.message,
|
|
223
|
+
fallbackTo: 'polling',
|
|
224
|
+
message: 'Notification mode failed - falling back to traditional polling'
|
|
225
|
+
});
|
|
226
|
+
// Fall back to regular polling
|
|
227
|
+
await this.processTimeHooks(timeEventCallback);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
this.logger.info('task-using-polling-mode', {
|
|
232
|
+
appId: this.store.appId,
|
|
233
|
+
storeType: this.store.constructor.name,
|
|
234
|
+
message: 'Time scout using traditional polling mode (notifications not available)'
|
|
235
|
+
});
|
|
236
|
+
// Use regular polling for non-PostgreSQL stores
|
|
237
|
+
await this.processTimeHooks(timeEventCallback);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Check if this is a PostgreSQL store
|
|
242
|
+
*/
|
|
243
|
+
isPostgresStore() {
|
|
244
|
+
return this.store.constructor.name === 'PostgresStoreService';
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Check if the store supports notifications
|
|
248
|
+
*/
|
|
249
|
+
supportsNotifications() {
|
|
250
|
+
return typeof this.store.startTimeScoutWithNotifications === 'function';
|
|
251
|
+
}
|
|
205
252
|
}
|
|
206
253
|
exports.TaskService = TaskService;
|