@hotmeshio/hotmesh 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -47
- package/build/index.d.ts +2 -1
- package/build/index.js +3 -1
- package/build/modules/errors.d.ts +1 -0
- package/build/modules/errors.js +1 -0
- package/build/package.json +4 -3
- package/build/services/activities/trigger.d.ts +1 -1
- package/build/services/activities/trigger.js +6 -3
- package/build/services/memflow/client.js +2 -1
- package/build/services/memflow/{context.d.ts → entity.d.ts} +34 -34
- package/build/services/memflow/{context.js → entity.js} +40 -40
- package/build/services/memflow/index.d.ts +2 -2
- package/build/services/memflow/index.js +2 -2
- package/build/services/memflow/schemas/factory.d.ts +1 -1
- package/build/services/memflow/schemas/factory.js +27 -1
- package/build/services/memflow/worker.js +1 -0
- package/build/services/memflow/workflow/common.d.ts +2 -2
- package/build/services/memflow/workflow/common.js +3 -3
- package/build/services/memflow/workflow/entityMethods.d.ts +14 -0
- package/build/services/memflow/workflow/{contextMethods.js → entityMethods.js} +11 -11
- package/build/services/memflow/workflow/execChild.js +2 -1
- package/build/services/memflow/workflow/index.d.ts +2 -2
- package/build/services/memflow/workflow/index.js +2 -2
- package/build/services/store/index.d.ts +1 -1
- package/build/services/store/providers/postgres/kvsql.d.ts +1 -1
- package/build/services/store/providers/postgres/kvtypes/hash.d.ts +1 -1
- package/build/services/store/providers/postgres/kvtypes/hash.js +8 -8
- package/build/services/store/providers/postgres/postgres.d.ts +1 -1
- package/build/services/store/providers/postgres/postgres.js +3 -2
- package/build/services/store/providers/redis/_base.d.ts +1 -1
- package/build/services/store/providers/redis/_base.js +1 -1
- package/build/types/activity.d.ts +2 -0
- package/build/types/error.d.ts +1 -0
- package/build/types/job.d.ts +7 -0
- package/build/types/provider.d.ts +1 -0
- package/index.ts +2 -2
- package/package.json +4 -3
- package/build/services/memflow/workflow/contextMethods.d.ts +0 -14
package/README.md
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
 
|
|
6
6
|
|
|
7
|
-
**HotMesh** is a Temporal-style workflow engine that runs natively on PostgreSQL — with a powerful twist: every workflow maintains
|
|
7
|
+
**HotMesh** is a Temporal-style workflow engine that runs natively on PostgreSQL — with a powerful twist: every workflow maintains permanent, state that persists independently of the workflow itself.
|
|
8
8
|
|
|
9
9
|
This means:
|
|
10
10
|
|
|
11
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
|
|
12
|
+
* These hooks can safely **read and write** to shared state.
|
|
13
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**.
|
|
14
14
|
|
|
15
15
|
---
|
|
@@ -18,7 +18,7 @@ This means:
|
|
|
18
18
|
|
|
19
19
|
1. 🚀 Quick Start
|
|
20
20
|
2. 🧠 How Permanent Memory Works
|
|
21
|
-
3. 🔌 Hooks &
|
|
21
|
+
3. 🔌 Hooks & Entity API
|
|
22
22
|
4. 🤖 Building Durable AI Agents
|
|
23
23
|
5. 🔬 Advanced Patterns & Recipes
|
|
24
24
|
6. 📚 Documentation & Links
|
|
@@ -51,9 +51,11 @@ async function main() {
|
|
|
51
51
|
|
|
52
52
|
// Kick off a workflow
|
|
53
53
|
const handle = await mf.workflow.start({
|
|
54
|
-
|
|
54
|
+
entity: 'user',
|
|
55
|
+
workflowName: 'userExample',
|
|
56
|
+
workflowId: 'jane@hotmesh.com',
|
|
55
57
|
args: ['Jane'],
|
|
56
|
-
taskQueue: '
|
|
58
|
+
taskQueue: 'entityqueue'
|
|
57
59
|
});
|
|
58
60
|
|
|
59
61
|
console.log('Result:', await handle.result());
|
|
@@ -66,17 +68,16 @@ main().catch(console.error);
|
|
|
66
68
|
|
|
67
69
|
## 🧠 How Permanent Memory Works
|
|
68
70
|
|
|
69
|
-
* **
|
|
71
|
+
* **Entity = persistent JSON record** – each workflow's memory is stored as a JSONB row in your Postgres database
|
|
70
72
|
* **Atomic operations** (`set`, `merge`, `append`, `increment`, `toggle`, `delete`, …)
|
|
71
73
|
* **Transactional** – every update participates in the workflow/DB transaction
|
|
72
74
|
* **Time-travel-safe** – full replay compatibility; side-effect detector guarantees determinism
|
|
73
75
|
* **Hook-friendly** – any worker with the record ID can attach and mutate its slice of the JSON
|
|
74
|
-
|
|
75
|
-
* Context data is stored as JSONB; add partial indexes for improved query analysis.
|
|
76
|
+
* **Index-friendly** - entity data is stored as JSONB; add partial indexes for improved query analysis.
|
|
76
77
|
|
|
77
78
|
**Example: Adding a Partial Index for Specific Entity Types**
|
|
78
79
|
```sql
|
|
79
|
-
-- Create a partial index for 'user' entities with specific
|
|
80
|
+
-- Create a partial index for 'user' entities with specific entity values
|
|
80
81
|
CREATE INDEX idx_user_premium ON your_app.jobs (id)
|
|
81
82
|
WHERE entity = 'user' AND (context->>'isPremium')::boolean = true;
|
|
82
83
|
```
|
|
@@ -84,42 +85,44 @@ This index will only be used for queries that match both conditions, making look
|
|
|
84
85
|
|
|
85
86
|
---
|
|
86
87
|
|
|
87
|
-
## 🔌 Hooks &
|
|
88
|
+
## 🔌 Hooks & Entity API – Full Example
|
|
89
|
+
|
|
90
|
+
HotMesh hooks are powerful because they can be called both internally (from within a workflow) and externally (from outside, even after the workflow completes). This means you can:
|
|
91
|
+
|
|
92
|
+
* Start a workflow that sets up initial state
|
|
93
|
+
* Have the workflow call some hooks internally
|
|
94
|
+
* Let the workflow complete
|
|
95
|
+
* Continue to update the workflow's entity state from the outside via hooks
|
|
96
|
+
* Build long-running processes that evolve over time
|
|
97
|
+
|
|
98
|
+
Here's a complete example showing both internal and external hook usage:
|
|
88
99
|
|
|
89
100
|
```typescript
|
|
90
101
|
import { MemFlow } from '@hotmeshio/hotmesh';
|
|
91
102
|
|
|
92
103
|
/* ------------ Main workflow ------------ */
|
|
93
|
-
export async function
|
|
94
|
-
//the
|
|
95
|
-
const
|
|
104
|
+
export async function userExample(name: string): Promise<any> {
|
|
105
|
+
//the entity method provides transactional, replayable access to shared job state
|
|
106
|
+
const entity = await MemFlow.workflow.entity();
|
|
96
107
|
|
|
97
|
-
//create the initial
|
|
98
|
-
await
|
|
108
|
+
//create the initial entity (even arrays are supported)
|
|
109
|
+
await entity.set({
|
|
99
110
|
user: { name },
|
|
100
111
|
hooks: {},
|
|
101
112
|
metrics: { count: 0 }
|
|
102
113
|
});
|
|
103
114
|
|
|
104
|
-
// Call
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
args: [name, 'hook2'],
|
|
116
|
-
signalId: 'hook2-complete',
|
|
117
|
-
})
|
|
118
|
-
]);
|
|
119
|
-
|
|
120
|
-
// merge here (or have the hooks merge in...everyone can access context)
|
|
121
|
-
await ctx.merge({ hooks: { r1, r2 } });
|
|
122
|
-
await ctx.increment('metrics.count', 2);
|
|
115
|
+
// Call one hook internally
|
|
116
|
+
const result1 = await MemFlow.workflow.execHook({
|
|
117
|
+
taskQueue: 'entityqueue',
|
|
118
|
+
workflowName: 'hook1',
|
|
119
|
+
args: [name, 'hook1'],
|
|
120
|
+
signalId: 'hook1-complete'
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// merge the result
|
|
124
|
+
await entity.merge({ hooks: { r1: result1 } });
|
|
125
|
+
await entity.increment('metrics.count', 1);
|
|
123
126
|
|
|
124
127
|
return "The main has completed; the db record persists and can be hydrated; hook in from the outside!";
|
|
125
128
|
}
|
|
@@ -131,10 +134,10 @@ export async function hook1(name: string, kind: string): Promise<any> {
|
|
|
131
134
|
await MemFlow.workflow.signal('hook1-complete', res);
|
|
132
135
|
}
|
|
133
136
|
|
|
134
|
-
/* ------------ Hook 2 (hooks can access shared job
|
|
137
|
+
/* ------------ Hook 2 (hooks can access shared job entity) ------------ */
|
|
135
138
|
export async function hook2(name: string, kind: string): Promise<void> {
|
|
136
|
-
const
|
|
137
|
-
await
|
|
139
|
+
const entity = await MemFlow.workflow.entity();
|
|
140
|
+
await entity.merge({ user: { lastSeen: new Date().toISOString() } });
|
|
138
141
|
await MemFlow.workflow.signal('hook2-complete', { ok: true });
|
|
139
142
|
}
|
|
140
143
|
|
|
@@ -151,17 +154,17 @@ async function startWorker() {
|
|
|
151
154
|
});
|
|
152
155
|
|
|
153
156
|
const worker = await mf.worker.create({
|
|
154
|
-
taskQueue: '
|
|
157
|
+
taskQueue: 'entityqueue',
|
|
155
158
|
workflow: example
|
|
156
159
|
});
|
|
157
160
|
|
|
158
161
|
await mf.worker.create({
|
|
159
|
-
taskQueue: '
|
|
162
|
+
taskQueue: 'entityqueue',
|
|
160
163
|
workflow: hook1
|
|
161
164
|
});
|
|
162
165
|
|
|
163
166
|
await mf.worker.create({
|
|
164
|
-
taskQueue: '
|
|
167
|
+
taskQueue: 'entityqueue',
|
|
165
168
|
workflow: hook2
|
|
166
169
|
});
|
|
167
170
|
|
|
@@ -169,19 +172,195 @@ async function startWorker() {
|
|
|
169
172
|
}
|
|
170
173
|
```
|
|
171
174
|
|
|
175
|
+
### The Power of External Hooks
|
|
176
|
+
|
|
177
|
+
One of HotMesh's most powerful features is that workflow entities remain accessible even after the main workflow completes. By providing the original workflow ID, any authorized client can:
|
|
178
|
+
|
|
179
|
+
* Hook into existing workflow entities
|
|
180
|
+
* Update state and trigger new processing
|
|
181
|
+
* Build evolving, long-running processes
|
|
182
|
+
* Enable human-in-the-loop workflows
|
|
183
|
+
* Create AI agents that learn over time
|
|
184
|
+
|
|
185
|
+
Here's how to hook into an existing workflow from the outside:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
/* ------------ External Hook Example ------------ */
|
|
189
|
+
async function externalHookExample() {
|
|
190
|
+
const client = new MemFlow.Client({
|
|
191
|
+
appId: 'my-app',
|
|
192
|
+
engine: {
|
|
193
|
+
connection: {
|
|
194
|
+
class: Postgres,
|
|
195
|
+
options: { connectionString: process.env.DATABASE_URL }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Start hook2 externally by providing the original workflow ID
|
|
201
|
+
await client.workflow.hook({
|
|
202
|
+
workflowId: 'jane@hotmesh.com', //id of the target workflow
|
|
203
|
+
taskQueue: 'entityqueue',
|
|
204
|
+
workflowName: 'hook2',
|
|
205
|
+
args: [name, 'external-hook']
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
172
210
|
---
|
|
173
211
|
|
|
174
212
|
## 🤖 Building Durable AI Agents
|
|
175
213
|
|
|
176
|
-
|
|
214
|
+
HotMesh's permanent memory enables a revolutionary approach to AI agents: **perspective-oriented intelligence**. Instead of monolithic agents, you build **multi-faceted agents** where:
|
|
215
|
+
|
|
216
|
+
* **Entity state** = the agent's living, evolving context
|
|
217
|
+
* **Hooks** = different perspectives that operate on that context
|
|
218
|
+
* **Child workflows** = specialized co-agents with their own entity types
|
|
219
|
+
* **Shared memory** = enables rich self-reflection and multi-agent collaboration
|
|
220
|
+
|
|
221
|
+
### Example: A Research Agent with Multiple Perspectives
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
/* ------------ Main Research Agent ------------ */
|
|
225
|
+
export async function researchAgent(query: string): Promise<any> {
|
|
226
|
+
const entity = await MemFlow.workflow.entity();
|
|
227
|
+
|
|
228
|
+
// Initialize agent context
|
|
229
|
+
await entity.set({
|
|
230
|
+
query,
|
|
231
|
+
findings: [],
|
|
232
|
+
perspectives: {},
|
|
233
|
+
confidence: 0,
|
|
234
|
+
status: 'researching'
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Spawn perspective hooks that operate on shared context
|
|
238
|
+
const optimisticView = MemFlow.workflow.execHook({
|
|
239
|
+
taskQueue: 'perspectives',
|
|
240
|
+
workflowName: 'optimisticPerspective',
|
|
241
|
+
args: [query]
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const skepticalView = MemFlow.workflow.execHook({
|
|
245
|
+
taskQueue: 'perspectives',
|
|
246
|
+
workflowName: 'skepticalPerspective',
|
|
247
|
+
args: [query]
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Spawn a fact-checker child agent with its own entity type
|
|
251
|
+
const factChecker = await MemFlow.workflow.execChild({
|
|
252
|
+
entity: 'fact-checker',
|
|
253
|
+
workflowName: 'factCheckAgent',
|
|
254
|
+
workflowId: `fact-check-${Date.now()}`,
|
|
255
|
+
args: [query],
|
|
256
|
+
taskQueue: 'agents'
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Wait for all perspectives to contribute
|
|
260
|
+
await Promise.all([optimisticView, skepticalView, factChecker]);
|
|
261
|
+
|
|
262
|
+
// Self-reflection: analyze conflicting perspectives
|
|
263
|
+
await MemFlow.workflow.execHook({
|
|
264
|
+
taskQueue: 'perspectives',
|
|
265
|
+
workflowName: 'synthesizePerspectives',
|
|
266
|
+
args: []
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const finalContext = await entity.get();
|
|
270
|
+
return finalContext;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* ------------ Optimistic Perspective Hook ------------ */
|
|
274
|
+
export async function optimisticPerspective(query: string): Promise<void> {
|
|
275
|
+
const entity = await MemFlow.workflow.entity();
|
|
276
|
+
|
|
277
|
+
// Optimistic perspective: look for supporting evidence
|
|
278
|
+
const findings = await searchForSupportingEvidence(query);
|
|
279
|
+
|
|
280
|
+
await entity.merge({
|
|
281
|
+
perspectives: {
|
|
282
|
+
optimistic: {
|
|
283
|
+
findings,
|
|
284
|
+
confidence: 0.8,
|
|
285
|
+
bias: 'Tends to emphasize positive evidence'
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ------------ Skeptical Perspective Hook ------------ */
|
|
292
|
+
export async function skepticalPerspective(query: string): Promise<void> {
|
|
293
|
+
const entity = await MemFlow.workflow.entity();
|
|
294
|
+
|
|
295
|
+
// Skeptical perspective: challenge assumptions
|
|
296
|
+
const counterEvidence = await searchForCounterEvidence(query);
|
|
297
|
+
|
|
298
|
+
await entity.merge({
|
|
299
|
+
perspectives: {
|
|
300
|
+
skeptical: {
|
|
301
|
+
counterEvidence,
|
|
302
|
+
confidence: 0.6,
|
|
303
|
+
bias: 'Challenges assumptions and seeks contradictory evidence'
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* ------------ Fact-Checker Child Agent ------------ */
|
|
310
|
+
export async function factCheckAgent(query: string): Promise<any> {
|
|
311
|
+
// This child has its own entity type for specialized fact-checking context
|
|
312
|
+
const entity = await MemFlow.workflow.entity();
|
|
313
|
+
|
|
314
|
+
await entity.set({
|
|
315
|
+
query,
|
|
316
|
+
sources: [],
|
|
317
|
+
verifications: [],
|
|
318
|
+
credibilityScore: 0
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Fact-checker can spawn its own specialized hooks
|
|
322
|
+
await MemFlow.workflow.execHook({
|
|
323
|
+
taskQueue: 'verification',
|
|
324
|
+
workflowName: 'verifySourceCredibility',
|
|
325
|
+
args: [query]
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return await entity.get();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* ------------ Synthesis Perspective Hook ------------ */
|
|
332
|
+
export async function synthesizePerspectives(): Promise<void> {
|
|
333
|
+
const entity = await MemFlow.workflow.entity();
|
|
334
|
+
const context = await entity.get();
|
|
335
|
+
|
|
336
|
+
// Analyze conflicting perspectives and synthesize
|
|
337
|
+
const synthesis = await analyzePerspectives(context.perspectives);
|
|
338
|
+
|
|
339
|
+
await entity.merge({
|
|
340
|
+
perspectives: {
|
|
341
|
+
synthesis: {
|
|
342
|
+
finalAssessment: synthesis,
|
|
343
|
+
confidence: calculateConfidence(context.perspectives),
|
|
344
|
+
reasoning: 'Balanced analysis of optimistic and skeptical viewpoints'
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
status: 'completed'
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### The Power of Perspective-Oriented Agents
|
|
353
|
+
|
|
354
|
+
This approach enables agents that can:
|
|
177
355
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
356
|
+
* **Question themselves** – different hooks challenge each other's assumptions
|
|
357
|
+
* **Spawn specialized co-agents** – child workflows with their own entity types tackle specific domains
|
|
358
|
+
* **Maintain rich context** – all perspectives contribute to a shared, evolving knowledge base
|
|
359
|
+
* **Scale horizontally** – each perspective can run on different workers
|
|
360
|
+
* **Learn continuously** – entity state accumulates insights across all interactions
|
|
361
|
+
* **Self-reflect** – synthesis hooks analyze conflicting perspectives and improve decision-making
|
|
182
362
|
|
|
183
|
-
Because every
|
|
184
|
-
restart, scale horizontally, and keep evolving their world-model indefinitely.
|
|
363
|
+
Because every perspective operates on the same durable entity, agents can pause, restart, and evolve their world-model indefinitely while maintaining coherent, multi-faceted intelligence.
|
|
185
364
|
|
|
186
365
|
---
|
|
187
366
|
|
package/build/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { MemFlow } from './services/memflow';
|
|
|
5
5
|
import { ClientService as Client } from './services/memflow/client';
|
|
6
6
|
import { ConnectionService as Connection } from './services/memflow/connection';
|
|
7
7
|
import { Search } from './services/memflow/search';
|
|
8
|
+
import { Entity } from './services/memflow/entity';
|
|
8
9
|
import { WorkerService as Worker } from './services/memflow/worker';
|
|
9
10
|
import { WorkflowService as workflow } from './services/memflow/workflow';
|
|
10
11
|
import { WorkflowHandleService as WorkflowHandle } from './services/memflow/handle';
|
|
@@ -21,5 +22,5 @@ import { RedisConnection as ConnectorIORedis } from './services/connector/provid
|
|
|
21
22
|
import { RedisConnection as ConnectorRedis } from './services/connector/providers/redis';
|
|
22
23
|
import { NatsConnection as ConnectorNATS } from './services/connector/providers/nats';
|
|
23
24
|
export { Connector, //factory
|
|
24
|
-
ConnectorIORedis, ConnectorNATS, ConnectorPostgres, ConnectorRedis, HotMesh, HotMeshConfig, MeshCall, MeshData, MemFlow, MeshOS, Client, Connection, proxyActivities, Search, Worker, workflow, WorkflowHandle, Enums, Errors, Utils, KeyStore, };
|
|
25
|
+
ConnectorIORedis, ConnectorNATS, ConnectorPostgres, ConnectorRedis, HotMesh, HotMeshConfig, MeshCall, MeshData, MemFlow, MeshOS, Client, Connection, proxyActivities, Search, Entity, Worker, workflow, WorkflowHandle, Enums, Errors, Utils, KeyStore, };
|
|
25
26
|
export * as Types from './types';
|
package/build/index.js
CHANGED
|
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.Types = exports.KeyStore = exports.Utils = exports.Errors = exports.Enums = exports.WorkflowHandle = exports.workflow = exports.Worker = exports.Search = exports.proxyActivities = exports.Connection = exports.Client = exports.MeshOS = exports.MemFlow = exports.MeshData = exports.MeshCall = exports.HotMesh = exports.ConnectorRedis = exports.ConnectorPostgres = exports.ConnectorNATS = exports.ConnectorIORedis = exports.Connector = void 0;
|
|
26
|
+
exports.Types = exports.KeyStore = exports.Utils = exports.Errors = exports.Enums = exports.WorkflowHandle = exports.workflow = exports.Worker = exports.Entity = exports.Search = exports.proxyActivities = exports.Connection = exports.Client = exports.MeshOS = exports.MemFlow = exports.MeshData = exports.MeshCall = exports.HotMesh = exports.ConnectorRedis = exports.ConnectorPostgres = exports.ConnectorNATS = exports.ConnectorIORedis = exports.Connector = void 0;
|
|
27
27
|
const hotmesh_1 = require("./services/hotmesh");
|
|
28
28
|
Object.defineProperty(exports, "HotMesh", { enumerable: true, get: function () { return hotmesh_1.HotMesh; } });
|
|
29
29
|
const meshcall_1 = require("./services/meshcall");
|
|
@@ -36,6 +36,8 @@ const connection_1 = require("./services/memflow/connection");
|
|
|
36
36
|
Object.defineProperty(exports, "Connection", { enumerable: true, get: function () { return connection_1.ConnectionService; } });
|
|
37
37
|
const search_1 = require("./services/memflow/search");
|
|
38
38
|
Object.defineProperty(exports, "Search", { enumerable: true, get: function () { return search_1.Search; } });
|
|
39
|
+
const entity_1 = require("./services/memflow/entity");
|
|
40
|
+
Object.defineProperty(exports, "Entity", { enumerable: true, get: function () { return entity_1.Entity; } });
|
|
39
41
|
const worker_1 = require("./services/memflow/worker");
|
|
40
42
|
Object.defineProperty(exports, "Worker", { enumerable: true, get: function () { return worker_1.WorkerService; } });
|
|
41
43
|
const workflow_1 = require("./services/memflow/workflow");
|
package/build/modules/errors.js
CHANGED
|
@@ -56,6 +56,7 @@ class MemFlowChildError extends Error {
|
|
|
56
56
|
this.persistent = params.persistent;
|
|
57
57
|
this.signalIn = params.signalIn;
|
|
58
58
|
this.originJobId = params.originJobId;
|
|
59
|
+
this.entity = params.entity;
|
|
59
60
|
this.index = params.index;
|
|
60
61
|
this.workflowDimension = params.workflowDimension;
|
|
61
62
|
this.code = enums_1.HMSH_CODE_MEMFLOW_CHILD;
|
package/build/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Permanent-Memory Workflows & AI Agents",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
7
|
-
"homepage": "https://
|
|
7
|
+
"homepage": "https://github.com/hotmeshio/sdk-typescript/",
|
|
8
8
|
"publishConfig": {
|
|
9
9
|
"access": "public"
|
|
10
10
|
},
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"test:memflow:collision": "NODE_ENV=test jest ./tests/memflow/collision/*.test.ts --detectOpenHandles --forceExit --verbose",
|
|
34
34
|
"test:memflow:fatal": "NODE_ENV=test jest ./tests/memflow/fatal/*.test.ts --detectOpenHandles --forceExit --verbose",
|
|
35
35
|
"test:memflow:goodbye": "NODE_ENV=test jest ./tests/memflow/goodbye/*.test.ts --detectOpenHandles --forceExit --verbose",
|
|
36
|
-
"test:memflow:
|
|
36
|
+
"test:memflow:entity": "NODE_ENV=test HMSH_LOGLEVEL=debug jest ./tests/memflow/entity/postgres.test.ts --detectOpenHandles --forceExit --verbose",
|
|
37
|
+
"test:memflow:agent": "NODE_ENV=test HMSH_LOGLEVEL=debug jest ./tests/memflow/agent/postgres.test.ts --detectOpenHandles --forceExit --verbose",
|
|
37
38
|
"test:memflow:hello": "HMSH_TELEMETRY=debug HMSH_LOGLEVEL=debug HMSH_IS_CLUSTER=true NODE_ENV=test jest ./tests/memflow/helloworld/*.test.ts --detectOpenHandles --forceExit --verbose",
|
|
38
39
|
"test:memflow:hook": "NODE_ENV=test jest ./tests/memflow/hook/*.test.ts --detectOpenHandles --forceExit --verbose",
|
|
39
40
|
"test:memflow:interrupt": "NODE_ENV=test jest ./tests/memflow/interrupt/*.test.ts --detectOpenHandles --forceExit --verbose",
|
|
@@ -31,7 +31,7 @@ declare class Trigger extends Activity {
|
|
|
31
31
|
getJobStatus(): number;
|
|
32
32
|
resolveJobId(context: Partial<JobState>): string;
|
|
33
33
|
resolveJobKey(context: Partial<JobState>): string;
|
|
34
|
-
setStateNX(status?: number): Promise<void>;
|
|
34
|
+
setStateNX(status?: number, entity?: string | undefined): Promise<void>;
|
|
35
35
|
setStats(transaction?: ProviderTransaction): Promise<void>;
|
|
36
36
|
}
|
|
37
37
|
export { Trigger };
|
|
@@ -9,6 +9,7 @@ const reporter_1 = require("../reporter");
|
|
|
9
9
|
const serializer_1 = require("../serializer");
|
|
10
10
|
const telemetry_1 = require("../telemetry");
|
|
11
11
|
const activity_1 = require("./activity");
|
|
12
|
+
const mapper_1 = require("../mapper");
|
|
12
13
|
class Trigger extends activity_1.Activity {
|
|
13
14
|
constructor(config, data, metadata, hook, engine, context) {
|
|
14
15
|
super(config, data, metadata, hook, engine, context);
|
|
@@ -27,7 +28,9 @@ class Trigger extends activity_1.Activity {
|
|
|
27
28
|
this.mapJobData();
|
|
28
29
|
this.adjacencyList = await this.filterAdjacent();
|
|
29
30
|
const initialStatus = this.initStatus(options, this.adjacencyList.length);
|
|
30
|
-
|
|
31
|
+
//config.entity is a pipe expression; if 'entity' exists, it will resolve
|
|
32
|
+
const resolvedEntity = new mapper_1.MapperService({ entity: this.config.entity }, this.context).mapRules()?.entity;
|
|
33
|
+
await this.setStateNX(initialStatus, options?.entity || resolvedEntity);
|
|
31
34
|
await this.setStatus(initialStatus);
|
|
32
35
|
this.bindSearchData(options);
|
|
33
36
|
this.bindMarkerData(options);
|
|
@@ -228,9 +231,9 @@ class Trigger extends activity_1.Activity {
|
|
|
228
231
|
const jobKey = this.config.stats?.key;
|
|
229
232
|
return jobKey ? pipe_1.Pipe.resolve(jobKey, context) : '';
|
|
230
233
|
}
|
|
231
|
-
async setStateNX(status) {
|
|
234
|
+
async setStateNX(status, entity) {
|
|
232
235
|
const jobId = this.context.metadata.jid;
|
|
233
|
-
if (!await this.store.setStateNX(jobId, this.engine.appId, status)) {
|
|
236
|
+
if (!await this.store.setStateNX(jobId, this.engine.appId, status, entity)) {
|
|
234
237
|
throw new errors_1.DuplicateJobError(jobId);
|
|
235
238
|
}
|
|
236
239
|
}
|
|
@@ -125,7 +125,7 @@ class ClientService {
|
|
|
125
125
|
*/
|
|
126
126
|
start: async (options) => {
|
|
127
127
|
const taskQueueName = options.taskQueue ?? options.entity;
|
|
128
|
-
const workflowName = options.entity ?? options.workflowName;
|
|
128
|
+
const workflowName = options.taskQueue ? options.workflowName : (options.entity ?? options.workflowName);
|
|
129
129
|
const trc = options.workflowTrace;
|
|
130
130
|
const spn = options.workflowSpan;
|
|
131
131
|
//hotmesh `topic` is equivalent to `queue+workflowname` pattern in other systems
|
|
@@ -151,6 +151,7 @@ class ClientService {
|
|
|
151
151
|
search: options?.search?.data,
|
|
152
152
|
marker: options?.marker,
|
|
153
153
|
pending: options?.pending,
|
|
154
|
+
entity: options?.entity,
|
|
154
155
|
});
|
|
155
156
|
return new handle_1.WorkflowHandleService(hotMeshClient, workflowTopic, jobId);
|
|
156
157
|
},
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import { HotMesh } from '../hotmesh';
|
|
2
2
|
import { SearchService } from '../search';
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
5
|
-
* JSONB data to a workflow's
|
|
4
|
+
* The Entity module provides methods for reading and writing
|
|
5
|
+
* JSONB data to a workflow's entity. The instance methods
|
|
6
6
|
* exposed by this class are available for use from within
|
|
7
7
|
* a running workflow.
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```typescript
|
|
11
|
-
* //
|
|
11
|
+
* //entityWorkflow.ts
|
|
12
12
|
* import { workflow } from '@hotmeshio/hotmesh';
|
|
13
13
|
*
|
|
14
|
-
* export async function
|
|
15
|
-
* const
|
|
16
|
-
* await
|
|
17
|
-
* await
|
|
18
|
-
* const user = await
|
|
14
|
+
* export async function entityExample(): Promise<void> {
|
|
15
|
+
* const entity = await workflow.entity();
|
|
16
|
+
* await entity.set({ user: { id: 123 } });
|
|
17
|
+
* await entity.merge({ user: { name: "John" } });
|
|
18
|
+
* const user = await entity.get("user");
|
|
19
19
|
* // user = { id: 123, name: "John" }
|
|
20
20
|
* }
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
|
-
export declare class
|
|
23
|
+
export declare class Entity {
|
|
24
24
|
/**
|
|
25
25
|
* @private
|
|
26
26
|
*/
|
|
@@ -56,84 +56,84 @@ export declare class Context {
|
|
|
56
56
|
*/
|
|
57
57
|
getSearchSessionGuid(): string;
|
|
58
58
|
/**
|
|
59
|
-
* Sets the entire
|
|
59
|
+
* Sets the entire entity object. This replaces any existing entity.
|
|
60
60
|
*
|
|
61
61
|
* @example
|
|
62
|
-
* const
|
|
63
|
-
* await
|
|
62
|
+
* const entity = await workflow.entity();
|
|
63
|
+
* await entity.set({ user: { id: 123, name: "John" } });
|
|
64
64
|
*/
|
|
65
65
|
set(value: any): Promise<any>;
|
|
66
66
|
/**
|
|
67
|
-
* Deep merges the provided object with the existing
|
|
67
|
+
* Deep merges the provided object with the existing entity
|
|
68
68
|
*
|
|
69
69
|
* @example
|
|
70
|
-
* const
|
|
71
|
-
* await
|
|
70
|
+
* const entity = await workflow.entity();
|
|
71
|
+
* await entity.merge({ user: { email: "john@example.com" } });
|
|
72
72
|
*/
|
|
73
73
|
merge<T>(value: T): Promise<T>;
|
|
74
74
|
/**
|
|
75
|
-
* Gets a value from the
|
|
75
|
+
* Gets a value from the entity by path
|
|
76
76
|
*
|
|
77
77
|
* @example
|
|
78
|
-
* const
|
|
79
|
-
* const user = await
|
|
80
|
-
* const email = await
|
|
78
|
+
* const entity = await workflow.entity();
|
|
79
|
+
* const user = await entity.get("user");
|
|
80
|
+
* const email = await entity.get("user.email");
|
|
81
81
|
*/
|
|
82
82
|
get(path?: string): Promise<any>;
|
|
83
83
|
/**
|
|
84
|
-
* Deletes a value from the
|
|
84
|
+
* Deletes a value from the entity by path
|
|
85
85
|
*
|
|
86
86
|
* @example
|
|
87
|
-
* const
|
|
88
|
-
* await
|
|
87
|
+
* const entity = await workflow.entity();
|
|
88
|
+
* await entity.delete("user.email");
|
|
89
89
|
*/
|
|
90
90
|
delete(path: string): Promise<any>;
|
|
91
91
|
/**
|
|
92
92
|
* Appends a value to an array at the specified path
|
|
93
93
|
*
|
|
94
94
|
* @example
|
|
95
|
-
* const
|
|
96
|
-
* await
|
|
95
|
+
* const entity = await workflow.entity();
|
|
96
|
+
* await entity.append("items", { id: 1, name: "New Item" });
|
|
97
97
|
*/
|
|
98
98
|
append(path: string, value: any): Promise<any[]>;
|
|
99
99
|
/**
|
|
100
100
|
* Prepends a value to an array at the specified path
|
|
101
101
|
*
|
|
102
102
|
* @example
|
|
103
|
-
* const
|
|
104
|
-
* await
|
|
103
|
+
* const entity = await workflow.entity();
|
|
104
|
+
* await entity.prepend("items", { id: 0, name: "First Item" });
|
|
105
105
|
*/
|
|
106
106
|
prepend(path: string, value: any): Promise<any[]>;
|
|
107
107
|
/**
|
|
108
108
|
* Removes an item from an array at the specified path and index
|
|
109
109
|
*
|
|
110
110
|
* @example
|
|
111
|
-
* const
|
|
112
|
-
* await
|
|
111
|
+
* const entity = await workflow.entity();
|
|
112
|
+
* await entity.remove("items", 0); // Remove first item
|
|
113
113
|
*/
|
|
114
114
|
remove(path: string, index: number): Promise<any[]>;
|
|
115
115
|
/**
|
|
116
116
|
* Increments a numeric value at the specified path
|
|
117
117
|
*
|
|
118
118
|
* @example
|
|
119
|
-
* const
|
|
120
|
-
* await
|
|
119
|
+
* const entity = await workflow.entity();
|
|
120
|
+
* await entity.increment("counter", 5);
|
|
121
121
|
*/
|
|
122
122
|
increment(path: string, value?: number): Promise<number>;
|
|
123
123
|
/**
|
|
124
124
|
* Toggles a boolean value at the specified path
|
|
125
125
|
*
|
|
126
126
|
* @example
|
|
127
|
-
* const
|
|
128
|
-
* await
|
|
127
|
+
* const entity = await workflow.entity();
|
|
128
|
+
* await entity.toggle("settings.enabled");
|
|
129
129
|
*/
|
|
130
130
|
toggle(path: string): Promise<boolean>;
|
|
131
131
|
/**
|
|
132
132
|
* Sets a value at the specified path only if it doesn't already exist
|
|
133
133
|
*
|
|
134
134
|
* @example
|
|
135
|
-
* const
|
|
136
|
-
* await
|
|
135
|
+
* const entity = await workflow.entity();
|
|
136
|
+
* await entity.setIfNotExists("user.id", 123);
|
|
137
137
|
*/
|
|
138
138
|
setIfNotExists(path: string, value: any): Promise<any>;
|
|
139
139
|
/**
|