@hotmeshio/hotmesh 0.21.0 → 0.22.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 +12 -129
- package/build/modules/utils.js +3 -0
- package/build/package.json +2 -1
- package/build/services/activities/hook.d.ts +178 -58
- package/build/services/activities/hook.js +244 -58
- package/build/services/activities/trigger.js +5 -1
- package/build/services/durable/client.d.ts +238 -66
- package/build/services/durable/client.js +309 -125
- package/build/services/durable/index.d.ts +0 -2
- package/build/services/durable/schemas/factory.js +40 -0
- package/build/services/durable/worker.js +5 -28
- package/build/services/durable/workflow/condition.d.ts +69 -37
- package/build/services/durable/workflow/condition.js +70 -39
- package/build/services/hotmesh/index.d.ts +31 -4
- package/build/services/hotmesh/index.js +31 -4
- 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/kvtables.js +83 -122
- package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +1 -1
- package/build/services/store/providers/postgres/kvtypes/hash/basic.js +8 -8
- package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +1 -1
- package/build/services/store/providers/postgres/postgres.d.ts +44 -157
- package/build/services/store/providers/postgres/postgres.js +480 -278
- package/build/types/activity.d.ts +2 -0
- package/build/types/hmsh_escalations.d.ts +212 -0
- package/build/types/index.d.ts +1 -1
- package/build/types/provider.d.ts +2 -0
- package/package.json +2 -1
- package/build/types/signal.d.ts +0 -147
- /package/build/types/{signal.js → hmsh_escalations.js} +0 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
export type { ConditionQueueConfig };
|
|
1
|
+
import { ConditionQueueConfig } from '../../../types/hmsh_escalations';
|
|
3
2
|
/**
|
|
4
3
|
* Pauses the workflow until a signal with the given `signalId` is received.
|
|
5
4
|
* The workflow suspends durably — it survives process restarts and will
|
|
@@ -7,64 +6,97 @@ export type { ConditionQueueConfig };
|
|
|
7
6
|
*
|
|
8
7
|
* `condition` is the **receive** side of the signal coordination pair.
|
|
9
8
|
* The **send** side is `signal()`, which can be called from another
|
|
10
|
-
* workflow, a hook function, or externally via `
|
|
9
|
+
* workflow, a hook function, or externally via `client.workflow.signal()`.
|
|
11
10
|
*
|
|
12
11
|
* On replay, `condition` returns the previously stored signal payload
|
|
13
|
-
* immediately
|
|
12
|
+
* immediately — no actual suspension occurs.
|
|
14
13
|
*
|
|
15
|
-
* ##
|
|
14
|
+
* ## Basic usage
|
|
16
15
|
*
|
|
17
16
|
* ```typescript
|
|
18
17
|
* import { Durable } from '@hotmeshio/hotmesh';
|
|
19
18
|
*
|
|
20
|
-
* // Human-in-the-loop approval pattern
|
|
21
19
|
* export async function approvalWorkflow(orderId: string): Promise<boolean> {
|
|
22
20
|
* const { submitForReview } = Durable.workflow.proxyActivities<typeof activities>();
|
|
23
|
-
*
|
|
24
21
|
* await submitForReview(orderId);
|
|
25
22
|
*
|
|
26
|
-
* // Pause
|
|
23
|
+
* // Pause until a human approves or rejects
|
|
27
24
|
* const decision = await Durable.workflow.condition<{ approved: boolean }>('approval');
|
|
28
|
-
*
|
|
29
25
|
* return decision.approved;
|
|
30
26
|
* }
|
|
31
27
|
*
|
|
32
|
-
* //
|
|
28
|
+
* // From an API handler or another workflow:
|
|
33
29
|
* await client.workflow.signal('approval', { approved: true });
|
|
34
30
|
* ```
|
|
35
31
|
*
|
|
32
|
+
* ## With timeout
|
|
33
|
+
*
|
|
34
|
+
* Pass a duration string as the second argument to set a deadline.
|
|
35
|
+
* `condition` returns `false` if the timeout fires before a signal arrives.
|
|
36
|
+
*
|
|
36
37
|
* ```typescript
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* return [name, score];
|
|
44
|
-
* }
|
|
38
|
+
* const decision = await Durable.workflow.condition<{ approved: boolean }>(
|
|
39
|
+
* 'approval',
|
|
40
|
+
* '72h', // give reviewers 72 hours; returns false on timeout
|
|
41
|
+
* );
|
|
42
|
+
* if (decision === false) return 'auto-rejected-timeout';
|
|
43
|
+
* return decision.approved ? 'approved' : 'rejected';
|
|
45
44
|
* ```
|
|
46
45
|
*
|
|
46
|
+
* ## With escalation queue config
|
|
47
|
+
*
|
|
48
|
+
* Pass a {@link ConditionQueueConfig} as the second argument to surface the
|
|
49
|
+
* pause as a claimable row in `public.hmsh_escalations`. The INSERT is
|
|
50
|
+
* committed atomically with the workflow checkpoint — one write, no
|
|
51
|
+
* enrichment step, no secondary round-trip.
|
|
52
|
+
*
|
|
47
53
|
* ```typescript
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
54
|
+
* const decision = await Durable.workflow.condition<{ approved: boolean }>(
|
|
55
|
+
* 'manager-approval',
|
|
56
|
+
* {
|
|
57
|
+
* role: 'manager',
|
|
58
|
+
* type: 'order-approval',
|
|
59
|
+
* subtype: 'regional',
|
|
60
|
+
* priority: 2,
|
|
61
|
+
* description: 'Approve or reject the regional order',
|
|
62
|
+
* metadata: { orderId, region },
|
|
63
|
+
* envelope: { instructions: 'Review the attached order' },
|
|
64
|
+
* },
|
|
65
|
+
* );
|
|
66
|
+
*
|
|
67
|
+
* // Elsewhere: list, claim, then resolve (resumes the workflow)
|
|
68
|
+
* const [item] = await client.escalations.list({ role: 'manager', status: 'pending' });
|
|
69
|
+
* await client.escalations.claim({ id: item.id, assignee: 'alice@company.com' });
|
|
70
|
+
* await client.escalations.resolve({ id: item.id, resolverPayload: { approved: true } });
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* ## Fan-in: wait for multiple signals in parallel
|
|
74
|
+
*
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const [name, score] = await Promise.all([
|
|
77
|
+
* Durable.workflow.condition<string>('name-signal'),
|
|
78
|
+
* Durable.workflow.condition<number>('score-signal'),
|
|
79
|
+
* ]);
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* ## Paired with hook: spawn work, wait for its signal
|
|
83
|
+
*
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const signalId = `result-${Durable.workflow.random()}`;
|
|
86
|
+
* await Durable.workflow.hook({
|
|
87
|
+
* taskQueue: 'processors',
|
|
88
|
+
* workflowName: 'processItem',
|
|
89
|
+
* args: [input, signalId],
|
|
90
|
+
* });
|
|
91
|
+
* return await Durable.workflow.condition<string>(signalId);
|
|
62
92
|
* ```
|
|
63
93
|
*
|
|
64
94
|
* @template T - The type of data expected in the signal payload.
|
|
65
|
-
* @param
|
|
66
|
-
* @param
|
|
67
|
-
*
|
|
68
|
-
*
|
|
95
|
+
* @param signalId - A unique signal identifier shared by the sender and receiver.
|
|
96
|
+
* @param timeoutOrConfig - Optional timeout string (e.g. `'30s'`, `'24h'`) OR a
|
|
97
|
+
* {@link ConditionQueueConfig} that writes one row to `public.hmsh_escalations`
|
|
98
|
+
* atomically at suspension time. Cannot specify both; use the config object's
|
|
99
|
+
* `expiresAt` field for deadline enforcement when an escalation is involved.
|
|
100
|
+
* @returns The signal payload, or `false` if a timeout string was given and it expired.
|
|
69
101
|
*/
|
|
70
|
-
export declare function condition<T>(signalId: string, timeoutOrConfig?: string | ConditionQueueConfig
|
|
102
|
+
export declare function condition<T>(signalId: string, timeoutOrConfig?: string | ConditionQueueConfig): Promise<T | false>;
|
|
@@ -11,71 +11,102 @@ const didRun_1 = require("./didRun");
|
|
|
11
11
|
*
|
|
12
12
|
* `condition` is the **receive** side of the signal coordination pair.
|
|
13
13
|
* The **send** side is `signal()`, which can be called from another
|
|
14
|
-
* workflow, a hook function, or externally via `
|
|
14
|
+
* workflow, a hook function, or externally via `client.workflow.signal()`.
|
|
15
15
|
*
|
|
16
16
|
* On replay, `condition` returns the previously stored signal payload
|
|
17
|
-
* immediately
|
|
17
|
+
* immediately — no actual suspension occurs.
|
|
18
18
|
*
|
|
19
|
-
* ##
|
|
19
|
+
* ## Basic usage
|
|
20
20
|
*
|
|
21
21
|
* ```typescript
|
|
22
22
|
* import { Durable } from '@hotmeshio/hotmesh';
|
|
23
23
|
*
|
|
24
|
-
* // Human-in-the-loop approval pattern
|
|
25
24
|
* export async function approvalWorkflow(orderId: string): Promise<boolean> {
|
|
26
25
|
* const { submitForReview } = Durable.workflow.proxyActivities<typeof activities>();
|
|
27
|
-
*
|
|
28
26
|
* await submitForReview(orderId);
|
|
29
27
|
*
|
|
30
|
-
* // Pause
|
|
28
|
+
* // Pause until a human approves or rejects
|
|
31
29
|
* const decision = await Durable.workflow.condition<{ approved: boolean }>('approval');
|
|
32
|
-
*
|
|
33
30
|
* return decision.approved;
|
|
34
31
|
* }
|
|
35
32
|
*
|
|
36
|
-
* //
|
|
33
|
+
* // From an API handler or another workflow:
|
|
37
34
|
* await client.workflow.signal('approval', { approved: true });
|
|
38
35
|
* ```
|
|
39
36
|
*
|
|
37
|
+
* ## With timeout
|
|
38
|
+
*
|
|
39
|
+
* Pass a duration string as the second argument to set a deadline.
|
|
40
|
+
* `condition` returns `false` if the timeout fires before a signal arrives.
|
|
41
|
+
*
|
|
40
42
|
* ```typescript
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* return [name, score];
|
|
48
|
-
* }
|
|
43
|
+
* const decision = await Durable.workflow.condition<{ approved: boolean }>(
|
|
44
|
+
* 'approval',
|
|
45
|
+
* '72h', // give reviewers 72 hours; returns false on timeout
|
|
46
|
+
* );
|
|
47
|
+
* if (decision === false) return 'auto-rejected-timeout';
|
|
48
|
+
* return decision.approved ? 'approved' : 'rejected';
|
|
49
49
|
* ```
|
|
50
50
|
*
|
|
51
|
+
* ## With escalation queue config
|
|
52
|
+
*
|
|
53
|
+
* Pass a {@link ConditionQueueConfig} as the second argument to surface the
|
|
54
|
+
* pause as a claimable row in `public.hmsh_escalations`. The INSERT is
|
|
55
|
+
* committed atomically with the workflow checkpoint — one write, no
|
|
56
|
+
* enrichment step, no secondary round-trip.
|
|
57
|
+
*
|
|
51
58
|
* ```typescript
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
59
|
+
* const decision = await Durable.workflow.condition<{ approved: boolean }>(
|
|
60
|
+
* 'manager-approval',
|
|
61
|
+
* {
|
|
62
|
+
* role: 'manager',
|
|
63
|
+
* type: 'order-approval',
|
|
64
|
+
* subtype: 'regional',
|
|
65
|
+
* priority: 2,
|
|
66
|
+
* description: 'Approve or reject the regional order',
|
|
67
|
+
* metadata: { orderId, region },
|
|
68
|
+
* envelope: { instructions: 'Review the attached order' },
|
|
69
|
+
* },
|
|
70
|
+
* );
|
|
71
|
+
*
|
|
72
|
+
* // Elsewhere: list, claim, then resolve (resumes the workflow)
|
|
73
|
+
* const [item] = await client.escalations.list({ role: 'manager', status: 'pending' });
|
|
74
|
+
* await client.escalations.claim({ id: item.id, assignee: 'alice@company.com' });
|
|
75
|
+
* await client.escalations.resolve({ id: item.id, resolverPayload: { approved: true } });
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* ## Fan-in: wait for multiple signals in parallel
|
|
79
|
+
*
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const [name, score] = await Promise.all([
|
|
82
|
+
* Durable.workflow.condition<string>('name-signal'),
|
|
83
|
+
* Durable.workflow.condition<number>('score-signal'),
|
|
84
|
+
* ]);
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* ## Paired with hook: spawn work, wait for its signal
|
|
88
|
+
*
|
|
89
|
+
* ```typescript
|
|
90
|
+
* const signalId = `result-${Durable.workflow.random()}`;
|
|
91
|
+
* await Durable.workflow.hook({
|
|
92
|
+
* taskQueue: 'processors',
|
|
93
|
+
* workflowName: 'processItem',
|
|
94
|
+
* args: [input, signalId],
|
|
95
|
+
* });
|
|
96
|
+
* return await Durable.workflow.condition<string>(signalId);
|
|
66
97
|
* ```
|
|
67
98
|
*
|
|
68
99
|
* @template T - The type of data expected in the signal payload.
|
|
69
|
-
* @param
|
|
70
|
-
* @param
|
|
71
|
-
*
|
|
72
|
-
*
|
|
100
|
+
* @param signalId - A unique signal identifier shared by the sender and receiver.
|
|
101
|
+
* @param timeoutOrConfig - Optional timeout string (e.g. `'30s'`, `'24h'`) OR a
|
|
102
|
+
* {@link ConditionQueueConfig} that writes one row to `public.hmsh_escalations`
|
|
103
|
+
* atomically at suspension time. Cannot specify both; use the config object's
|
|
104
|
+
* `expiresAt` field for deadline enforcement when an escalation is involved.
|
|
105
|
+
* @returns The signal payload, or `false` if a timeout string was given and it expired.
|
|
73
106
|
*/
|
|
74
|
-
async function condition(signalId, timeoutOrConfig
|
|
107
|
+
async function condition(signalId, timeoutOrConfig) {
|
|
75
108
|
const timeout = typeof timeoutOrConfig === 'string' ? timeoutOrConfig : undefined;
|
|
76
|
-
const
|
|
77
|
-
? timeoutOrConfig
|
|
78
|
-
: queueConfig;
|
|
109
|
+
const queueConfig = timeoutOrConfig && typeof timeoutOrConfig === 'object' ? timeoutOrConfig : undefined;
|
|
79
110
|
const [didRunAlready, execIndex, result] = await (0, didRun_1.didRun)('wait');
|
|
80
111
|
(0, cancellationScope_1.checkCancellation)();
|
|
81
112
|
if (didRunAlready) {
|
|
@@ -122,7 +153,7 @@ async function condition(signalId, timeoutOrConfig, queueConfig) {
|
|
|
122
153
|
type: 'DurableWaitForError',
|
|
123
154
|
code: common_1.HMSH_CODE_DURABLE_WAIT,
|
|
124
155
|
...(timeout ? { duration: (0, common_1.s)(timeout) } : {}),
|
|
125
|
-
...(
|
|
156
|
+
...(queueConfig ? { queueConfig } : {}),
|
|
126
157
|
};
|
|
127
158
|
interruptionRegistry.push(interruptionMessage);
|
|
128
159
|
await (0, common_1.sleepImmediate)();
|
|
@@ -320,11 +320,38 @@ declare class HotMesh {
|
|
|
320
320
|
*/
|
|
321
321
|
scrub(jobId: string): Promise<void>;
|
|
322
322
|
/**
|
|
323
|
-
* Sends a signal to a paused workflow,
|
|
324
|
-
*
|
|
323
|
+
* Sends a signal to a paused workflow, resuming its execution with the
|
|
324
|
+
* provided data. This is the low-level primitive used by both the Durable
|
|
325
|
+
* `client.workflow.signal()` wrapper and `client.escalations.resolve()`.
|
|
325
326
|
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
327
|
+
* The signal is matched to the waiting activity by the `id` field inside
|
|
328
|
+
* `data` — it must equal the value used in the hook rule's
|
|
329
|
+
* `conditions.match[0].expected` expression (typically the job ID).
|
|
330
|
+
*
|
|
331
|
+
* **YAML DAG hook example** — signal a workflow paused at a webhook:
|
|
332
|
+
*
|
|
333
|
+
* ```typescript
|
|
334
|
+
* // The hook activity is waiting on topic 'order.approval'
|
|
335
|
+
* // The hook rule expects: actual '{$self.hook.data.id}' === '{t1.output.data.id}'
|
|
336
|
+
* await hotMesh.signal('order.approval', { id: jobId, approved: true });
|
|
337
|
+
* ```
|
|
338
|
+
*
|
|
339
|
+
* **Durable workflow** — use `client.workflow.signal()` instead, which
|
|
340
|
+
* resolves the topic internally:
|
|
341
|
+
*
|
|
342
|
+
* ```typescript
|
|
343
|
+
* await client.workflow.signal('manager-approval', { approved: true });
|
|
344
|
+
* ```
|
|
345
|
+
*
|
|
346
|
+
* **With escalation** — use `client.escalations.resolve()` to atomically
|
|
347
|
+
* deliver the signal and mark the escalation row resolved:
|
|
348
|
+
*
|
|
349
|
+
* ```typescript
|
|
350
|
+
* await client.escalations.resolve({ id: escalationId, resolverPayload: { approved: true } });
|
|
351
|
+
* ```
|
|
352
|
+
*
|
|
353
|
+
* @param topic - The signal topic (must match a deployed hook rule).
|
|
354
|
+
* @param data - Signal payload. Must contain `id` matching the hook rule's expected value.
|
|
328
355
|
*/
|
|
329
356
|
signal(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
|
|
330
357
|
/** @private */
|
|
@@ -428,11 +428,38 @@ class HotMesh {
|
|
|
428
428
|
return Jobs.scrub(this, jobId);
|
|
429
429
|
}
|
|
430
430
|
/**
|
|
431
|
-
* Sends a signal to a paused workflow,
|
|
432
|
-
*
|
|
431
|
+
* Sends a signal to a paused workflow, resuming its execution with the
|
|
432
|
+
* provided data. This is the low-level primitive used by both the Durable
|
|
433
|
+
* `client.workflow.signal()` wrapper and `client.escalations.resolve()`.
|
|
433
434
|
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
435
|
+
* The signal is matched to the waiting activity by the `id` field inside
|
|
436
|
+
* `data` — it must equal the value used in the hook rule's
|
|
437
|
+
* `conditions.match[0].expected` expression (typically the job ID).
|
|
438
|
+
*
|
|
439
|
+
* **YAML DAG hook example** — signal a workflow paused at a webhook:
|
|
440
|
+
*
|
|
441
|
+
* ```typescript
|
|
442
|
+
* // The hook activity is waiting on topic 'order.approval'
|
|
443
|
+
* // The hook rule expects: actual '{$self.hook.data.id}' === '{t1.output.data.id}'
|
|
444
|
+
* await hotMesh.signal('order.approval', { id: jobId, approved: true });
|
|
445
|
+
* ```
|
|
446
|
+
*
|
|
447
|
+
* **Durable workflow** — use `client.workflow.signal()` instead, which
|
|
448
|
+
* resolves the topic internally:
|
|
449
|
+
*
|
|
450
|
+
* ```typescript
|
|
451
|
+
* await client.workflow.signal('manager-approval', { approved: true });
|
|
452
|
+
* ```
|
|
453
|
+
*
|
|
454
|
+
* **With escalation** — use `client.escalations.resolve()` to atomically
|
|
455
|
+
* deliver the signal and mark the escalation row resolved:
|
|
456
|
+
*
|
|
457
|
+
* ```typescript
|
|
458
|
+
* await client.escalations.resolve({ id: escalationId, resolverPayload: { approved: true } });
|
|
459
|
+
* ```
|
|
460
|
+
*
|
|
461
|
+
* @param topic - The signal topic (must match a deployed hook rule).
|
|
462
|
+
* @param data - Signal payload. Must contain `id` matching the hook rule's expected value.
|
|
436
463
|
*/
|
|
437
464
|
async signal(topic, data, status, code) {
|
|
438
465
|
return Jobs.signal(this, topic, data, status, code);
|
|
@@ -45,7 +45,7 @@ declare abstract class StoreService<Provider extends ProviderClient, Transaction
|
|
|
45
45
|
jobId: string, appId: string, guidField: string, //
|
|
46
46
|
guidWeight: number, transaction?: ProviderTransaction): Promise<any>;
|
|
47
47
|
abstract getStatus(jobId: string, appId: string): Promise<number>;
|
|
48
|
-
abstract setStateNX(jobId: string, appId: string, status?: number, entity?: string, transaction?: ProviderTransaction): Promise<boolean>;
|
|
48
|
+
abstract setStateNX(jobId: string, appId: string, status?: number, entity?: string, transaction?: ProviderTransaction, originId?: string, parentId?: string): Promise<boolean>;
|
|
49
49
|
abstract setState(state: StringAnyType, status: number | null, jobId: string, symbolNames: string[], dIds: StringStringType, transaction?: TransactionProvider): Promise<string>;
|
|
50
50
|
abstract getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
|
|
51
51
|
abstract getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined>;
|
|
@@ -59,7 +59,7 @@ export declare class KVSQL {
|
|
|
59
59
|
setnxex: (key: string, value: string, delay: number, multi?: ProviderTransaction) => Promise<boolean>;
|
|
60
60
|
hset: (key: string, fields: Record<string, string>, options?: import("./kvtypes/hash/index").HSetOptions, multi?: any) => Promise<any>;
|
|
61
61
|
_hset: (key: string, fields: Record<string, string>, options?: import("./kvtypes/hash/index").HSetOptions) => import("./kvtypes/hash/types").SqlResult;
|
|
62
|
-
hsetnx: (key: string, field: string, value: string, multi?: ProviderTransaction, entity?: string) => Promise<number>;
|
|
62
|
+
hsetnx: (key: string, field: string, value: string, multi?: ProviderTransaction, entity?: string, originId?: string, parentId?: string) => Promise<number>;
|
|
63
63
|
hget: (key: string, field: string, multi?: ProviderTransaction) => Promise<string>;
|
|
64
64
|
_hget: (key: string, field: string) => import("./kvtypes/hash/types").SqlResult;
|
|
65
65
|
hdel: (key: string, fields: string[], multi?: unknown) => Promise<number>;
|
|
@@ -166,65 +166,6 @@ const KVTables = (context) => ({
|
|
|
166
166
|
ON ${jobsTable} (key) WHERE is_live;
|
|
167
167
|
`);
|
|
168
168
|
}
|
|
169
|
-
// v0.21.0: signal queue table for first-class HITL escalation primitives
|
|
170
|
-
const sigTable = `${schemaName}.hotmesh_signals`;
|
|
171
|
-
const { rows: sigRows } = await client.query(`SELECT to_regclass('${sigTable}') AS tbl`);
|
|
172
|
-
if (!sigRows[0].tbl) {
|
|
173
|
-
await client.query(`
|
|
174
|
-
CREATE TABLE IF NOT EXISTS ${sigTable} (
|
|
175
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
176
|
-
namespace TEXT NOT NULL,
|
|
177
|
-
app_id TEXT NOT NULL,
|
|
178
|
-
signal_key TEXT NOT NULL,
|
|
179
|
-
workflow_id TEXT NOT NULL,
|
|
180
|
-
job_id TEXT,
|
|
181
|
-
topic TEXT,
|
|
182
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
183
|
-
role TEXT,
|
|
184
|
-
type TEXT,
|
|
185
|
-
subtype TEXT,
|
|
186
|
-
priority INT NOT NULL DEFAULT 5,
|
|
187
|
-
description TEXT,
|
|
188
|
-
task_queue TEXT,
|
|
189
|
-
workflow_type TEXT,
|
|
190
|
-
assigned_to TEXT,
|
|
191
|
-
claimed_at TIMESTAMPTZ,
|
|
192
|
-
claim_expires_at TIMESTAMPTZ,
|
|
193
|
-
resolved_at TIMESTAMPTZ,
|
|
194
|
-
resolver_payload JSONB,
|
|
195
|
-
envelope JSONB,
|
|
196
|
-
metadata JSONB,
|
|
197
|
-
expires_at TIMESTAMPTZ,
|
|
198
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
199
|
-
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
200
|
-
UNIQUE(namespace, app_id, signal_key)
|
|
201
|
-
);
|
|
202
|
-
`);
|
|
203
|
-
await client.query(`
|
|
204
|
-
CREATE INDEX IF NOT EXISTS idx_hmsig_namespace_status
|
|
205
|
-
ON ${sigTable}(namespace, app_id, status);
|
|
206
|
-
`);
|
|
207
|
-
await client.query(`
|
|
208
|
-
CREATE INDEX IF NOT EXISTS idx_hmsig_signal_key
|
|
209
|
-
ON ${sigTable}(namespace, app_id, signal_key);
|
|
210
|
-
`);
|
|
211
|
-
await client.query(`
|
|
212
|
-
CREATE INDEX IF NOT EXISTS idx_hmsig_role_status
|
|
213
|
-
ON ${sigTable}(namespace, app_id, role, status);
|
|
214
|
-
`);
|
|
215
|
-
await client.query(`
|
|
216
|
-
CREATE INDEX IF NOT EXISTS idx_hmsig_task_queue
|
|
217
|
-
ON ${sigTable}(namespace, app_id, task_queue, status);
|
|
218
|
-
`);
|
|
219
|
-
await client.query(`
|
|
220
|
-
CREATE INDEX IF NOT EXISTS idx_hmsig_metadata_gin
|
|
221
|
-
ON ${sigTable} USING GIN(metadata);
|
|
222
|
-
`);
|
|
223
|
-
await client.query(`
|
|
224
|
-
CREATE INDEX IF NOT EXISTS idx_hmsig_claim_expiry
|
|
225
|
-
ON ${sigTable}(claim_expires_at) WHERE status = 'claimed';
|
|
226
|
-
`);
|
|
227
|
-
}
|
|
228
169
|
},
|
|
229
170
|
async createTables(client, appName) {
|
|
230
171
|
try {
|
|
@@ -259,6 +200,87 @@ const KVTables = (context) => ({
|
|
|
259
200
|
deployed_at TIMESTAMPTZ DEFAULT NOW(),
|
|
260
201
|
PRIMARY KEY (app_id, version)
|
|
261
202
|
);
|
|
203
|
+
`);
|
|
204
|
+
await client.query(`
|
|
205
|
+
CREATE TABLE IF NOT EXISTS public.hmsh_escalations (
|
|
206
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
207
|
+
namespace TEXT NOT NULL,
|
|
208
|
+
app_id TEXT NOT NULL,
|
|
209
|
+
signal_key TEXT,
|
|
210
|
+
topic TEXT,
|
|
211
|
+
workflow_id TEXT,
|
|
212
|
+
task_queue TEXT,
|
|
213
|
+
workflow_type TEXT,
|
|
214
|
+
type TEXT,
|
|
215
|
+
subtype TEXT,
|
|
216
|
+
entity TEXT,
|
|
217
|
+
description TEXT,
|
|
218
|
+
role TEXT,
|
|
219
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
220
|
+
priority INT NOT NULL DEFAULT 5,
|
|
221
|
+
assigned_to TEXT,
|
|
222
|
+
assigned_until TIMESTAMPTZ,
|
|
223
|
+
claimed_at TIMESTAMPTZ,
|
|
224
|
+
claim_expires_at TIMESTAMPTZ,
|
|
225
|
+
resolved_at TIMESTAMPTZ,
|
|
226
|
+
escalation_payload JSONB,
|
|
227
|
+
resolver_payload JSONB,
|
|
228
|
+
envelope JSONB,
|
|
229
|
+
metadata JSONB,
|
|
230
|
+
origin_id TEXT,
|
|
231
|
+
parent_id TEXT,
|
|
232
|
+
initiated_by TEXT,
|
|
233
|
+
created_by TEXT,
|
|
234
|
+
milestones JSONB NOT NULL DEFAULT '[]',
|
|
235
|
+
trace_id TEXT,
|
|
236
|
+
span_id TEXT,
|
|
237
|
+
expires_at TIMESTAMPTZ,
|
|
238
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
239
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
240
|
+
);
|
|
241
|
+
`);
|
|
242
|
+
await client.query(`
|
|
243
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_hmsh_esc_signal_key
|
|
244
|
+
ON public.hmsh_escalations(namespace, app_id, signal_key)
|
|
245
|
+
WHERE signal_key IS NOT NULL;
|
|
246
|
+
`);
|
|
247
|
+
await client.query(`
|
|
248
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_available
|
|
249
|
+
ON public.hmsh_escalations(namespace, app_id, role, priority ASC, created_at ASC)
|
|
250
|
+
WHERE status = 'pending';
|
|
251
|
+
`);
|
|
252
|
+
await client.query(`
|
|
253
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_available_expiry
|
|
254
|
+
ON public.hmsh_escalations(namespace, app_id, role, assigned_until, created_at DESC);
|
|
255
|
+
`);
|
|
256
|
+
await client.query(`
|
|
257
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_assigned
|
|
258
|
+
ON public.hmsh_escalations(assigned_to, assigned_until, created_at DESC)
|
|
259
|
+
WHERE status = 'claimed' AND assigned_to IS NOT NULL;
|
|
260
|
+
`);
|
|
261
|
+
await client.query(`
|
|
262
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_claim_expiry
|
|
263
|
+
ON public.hmsh_escalations(claim_expires_at)
|
|
264
|
+
WHERE status = 'claimed';
|
|
265
|
+
`);
|
|
266
|
+
await client.query(`
|
|
267
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_entity
|
|
268
|
+
ON public.hmsh_escalations(namespace, app_id, entity, created_at DESC)
|
|
269
|
+
WHERE entity IS NOT NULL;
|
|
270
|
+
`);
|
|
271
|
+
await client.query(`
|
|
272
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_workflow
|
|
273
|
+
ON public.hmsh_escalations(workflow_id)
|
|
274
|
+
WHERE workflow_id IS NOT NULL;
|
|
275
|
+
`);
|
|
276
|
+
await client.query(`
|
|
277
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_origin
|
|
278
|
+
ON public.hmsh_escalations(origin_id)
|
|
279
|
+
WHERE origin_id IS NOT NULL;
|
|
280
|
+
`);
|
|
281
|
+
await client.query(`
|
|
282
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_metadata
|
|
283
|
+
ON public.hmsh_escalations USING GIN(metadata jsonb_path_ops);
|
|
262
284
|
`);
|
|
263
285
|
break;
|
|
264
286
|
case 'relational_connection':
|
|
@@ -320,6 +342,8 @@ const KVTables = (context) => ({
|
|
|
320
342
|
id UUID DEFAULT gen_random_uuid(),
|
|
321
343
|
key TEXT NOT NULL,
|
|
322
344
|
entity TEXT,
|
|
345
|
+
origin_id TEXT,
|
|
346
|
+
parent_id TEXT,
|
|
323
347
|
status INTEGER NOT NULL,
|
|
324
348
|
context JSONB DEFAULT '{}',
|
|
325
349
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
@@ -535,64 +559,6 @@ const KVTables = (context) => ({
|
|
|
535
559
|
ON ${fullTableName} (key, score, member);
|
|
536
560
|
`);
|
|
537
561
|
break;
|
|
538
|
-
case 'signal_queue': {
|
|
539
|
-
const tbl = fullTableName;
|
|
540
|
-
await client.query(`
|
|
541
|
-
CREATE TABLE IF NOT EXISTS ${tbl} (
|
|
542
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
543
|
-
namespace TEXT NOT NULL,
|
|
544
|
-
app_id TEXT NOT NULL,
|
|
545
|
-
signal_key TEXT NOT NULL,
|
|
546
|
-
workflow_id TEXT NOT NULL,
|
|
547
|
-
job_id TEXT,
|
|
548
|
-
topic TEXT,
|
|
549
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
550
|
-
role TEXT,
|
|
551
|
-
type TEXT,
|
|
552
|
-
subtype TEXT,
|
|
553
|
-
priority INT NOT NULL DEFAULT 5,
|
|
554
|
-
description TEXT,
|
|
555
|
-
task_queue TEXT,
|
|
556
|
-
workflow_type TEXT,
|
|
557
|
-
assigned_to TEXT,
|
|
558
|
-
claimed_at TIMESTAMPTZ,
|
|
559
|
-
claim_expires_at TIMESTAMPTZ,
|
|
560
|
-
resolved_at TIMESTAMPTZ,
|
|
561
|
-
resolver_payload JSONB,
|
|
562
|
-
envelope JSONB,
|
|
563
|
-
metadata JSONB,
|
|
564
|
-
expires_at TIMESTAMPTZ,
|
|
565
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
566
|
-
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
567
|
-
UNIQUE(namespace, app_id, signal_key)
|
|
568
|
-
);
|
|
569
|
-
`);
|
|
570
|
-
await client.query(`
|
|
571
|
-
CREATE INDEX IF NOT EXISTS idx_hmsig_namespace_status
|
|
572
|
-
ON ${tbl}(namespace, app_id, status);
|
|
573
|
-
`);
|
|
574
|
-
await client.query(`
|
|
575
|
-
CREATE INDEX IF NOT EXISTS idx_hmsig_signal_key
|
|
576
|
-
ON ${tbl}(namespace, app_id, signal_key);
|
|
577
|
-
`);
|
|
578
|
-
await client.query(`
|
|
579
|
-
CREATE INDEX IF NOT EXISTS idx_hmsig_role_status
|
|
580
|
-
ON ${tbl}(namespace, app_id, role, status);
|
|
581
|
-
`);
|
|
582
|
-
await client.query(`
|
|
583
|
-
CREATE INDEX IF NOT EXISTS idx_hmsig_task_queue
|
|
584
|
-
ON ${tbl}(namespace, app_id, task_queue, status);
|
|
585
|
-
`);
|
|
586
|
-
await client.query(`
|
|
587
|
-
CREATE INDEX IF NOT EXISTS idx_hmsig_metadata_gin
|
|
588
|
-
ON ${tbl} USING GIN(metadata);
|
|
589
|
-
`);
|
|
590
|
-
await client.query(`
|
|
591
|
-
CREATE INDEX IF NOT EXISTS idx_hmsig_claim_expiry
|
|
592
|
-
ON ${tbl}(claim_expires_at) WHERE status = 'claimed';
|
|
593
|
-
`);
|
|
594
|
-
break;
|
|
595
|
-
}
|
|
596
562
|
default:
|
|
597
563
|
context.logger.warn(`Unknown table type for ${tableDef.name}`);
|
|
598
564
|
break;
|
|
@@ -710,11 +676,6 @@ const KVTables = (context) => ({
|
|
|
710
676
|
name: 'signal_registry',
|
|
711
677
|
type: 'string',
|
|
712
678
|
},
|
|
713
|
-
{
|
|
714
|
-
schema: schemaName,
|
|
715
|
-
name: 'hotmesh_signals',
|
|
716
|
-
type: 'signal_queue',
|
|
717
|
-
},
|
|
718
679
|
];
|
|
719
680
|
return tableDefinitions;
|
|
720
681
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HashContext, SqlResult, HSetOptions, ProviderTransaction } from './types';
|
|
2
2
|
export declare function createBasicOperations(context: HashContext['context']): {
|
|
3
|
-
hsetnx(key: string, field: string, value: string, multi?: ProviderTransaction, entity?: string): Promise<number>;
|
|
3
|
+
hsetnx(key: string, field: string, value: string, multi?: ProviderTransaction, entity?: string, originId?: string, parentId?: string): Promise<number>;
|
|
4
4
|
hset(key: string, fields: Record<string, string>, options?: HSetOptions, multi?: ProviderTransaction): Promise<number | any>;
|
|
5
5
|
hget(key: string, field: string, multi?: ProviderTransaction): Promise<string | null>;
|
|
6
6
|
hdel(key: string, fields: string[], multi?: unknown): Promise<number>;
|