@hotmeshio/hotmesh 0.21.1 → 0.22.1

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 (32) hide show
  1. package/README.md +12 -129
  2. package/build/modules/utils.d.ts +2 -0
  3. package/build/modules/utils.js +9 -1
  4. package/build/package.json +8 -2
  5. package/build/services/activities/hook.d.ts +178 -58
  6. package/build/services/activities/hook.js +244 -58
  7. package/build/services/activities/trigger.js +5 -1
  8. package/build/services/durable/client.d.ts +273 -67
  9. package/build/services/durable/client.js +351 -126
  10. package/build/services/durable/index.d.ts +7 -3
  11. package/build/services/durable/index.js +6 -0
  12. package/build/services/durable/schemas/factory.js +40 -0
  13. package/build/services/durable/worker.js +5 -28
  14. package/build/services/durable/workflow/condition.d.ts +69 -37
  15. package/build/services/durable/workflow/condition.js +70 -39
  16. package/build/services/hotmesh/index.d.ts +31 -4
  17. package/build/services/hotmesh/index.js +31 -4
  18. package/build/services/store/index.d.ts +1 -1
  19. package/build/services/store/providers/postgres/kvsql.d.ts +1 -1
  20. package/build/services/store/providers/postgres/kvtables.js +83 -122
  21. package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +1 -1
  22. package/build/services/store/providers/postgres/kvtypes/hash/basic.js +8 -8
  23. package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +1 -1
  24. package/build/services/store/providers/postgres/postgres.d.ts +51 -188
  25. package/build/services/store/providers/postgres/postgres.js +542 -285
  26. package/build/types/activity.d.ts +2 -0
  27. package/build/types/hmsh_escalations.d.ts +240 -0
  28. package/build/types/index.d.ts +1 -1
  29. package/build/types/provider.d.ts +2 -0
  30. package/package.json +9 -2
  31. package/build/types/signal.d.ts +0 -147
  32. /package/build/types/{signal.js → hmsh_escalations.js} +0 -0
@@ -14,6 +14,8 @@ interface BaseActivity {
14
14
  settings?: Record<string, any>;
15
15
  job?: Record<string, any>;
16
16
  hook?: Record<string, any>;
17
+ /** When present on a hook activity, writes a row to public.hmsh_escalations at suspension time. Values may use `@pipe` expressions resolved against the current job context. */
18
+ escalation?: Record<string, any>;
17
19
  telemetry?: Record<string, any>;
18
20
  emit?: boolean;
19
21
  sleep?: number;
@@ -0,0 +1,240 @@
1
+ export interface ConditionQueueConfig {
2
+ role?: string;
3
+ type?: string;
4
+ subtype?: string;
5
+ entity?: string;
6
+ priority?: number;
7
+ description?: string;
8
+ taskQueue?: string;
9
+ workflowType?: string;
10
+ originId?: string;
11
+ parentId?: string;
12
+ initiatedBy?: string;
13
+ traceId?: string;
14
+ spanId?: string;
15
+ /** GIN-indexed; put claim/filter keys here */
16
+ metadata?: Record<string, unknown>;
17
+ /** Unindexed display/form context for resolver UIs */
18
+ envelope?: Record<string, unknown>;
19
+ expiresAt?: Date;
20
+ }
21
+ export interface EscalationEntry {
22
+ id: string;
23
+ namespace: string;
24
+ app_id: string;
25
+ /** Job ID / Durable signalId; NULL for standalone (no-signal) escalations */
26
+ signal_key: string | null;
27
+ /** Hook topic for signal delivery */
28
+ topic: string | null;
29
+ workflow_id: string | null;
30
+ task_queue: string | null;
31
+ workflow_type: string | null;
32
+ type: string | null;
33
+ subtype: string | null;
34
+ entity: string | null;
35
+ description: string | null;
36
+ role: string | null;
37
+ status: 'pending' | 'claimed' | 'resolved' | 'cancelled' | 'expired';
38
+ priority: number;
39
+ assigned_to: string | null;
40
+ assigned_until: Date | null;
41
+ claimed_at: Date | null;
42
+ claim_expires_at: Date | null;
43
+ resolved_at: Date | null;
44
+ escalation_payload: Record<string, unknown> | null;
45
+ resolver_payload: Record<string, unknown> | null;
46
+ envelope: Record<string, unknown> | null;
47
+ metadata: Record<string, unknown> | null;
48
+ origin_id: string | null;
49
+ parent_id: string | null;
50
+ initiated_by: string | null;
51
+ created_by: string | null;
52
+ milestones: unknown[];
53
+ trace_id: string | null;
54
+ span_id: string | null;
55
+ expires_at: Date | null;
56
+ created_at: Date;
57
+ updated_at: Date;
58
+ }
59
+ /**
60
+ * Result of `claim()` — identifies whether failure was due to the row not
61
+ * existing (`not-found`) or existing but locked / in a non-claimable state
62
+ * (`conflict`). Distinguishing these lets callers decide whether to retry or
63
+ * surface an error to the user.
64
+ */
65
+ export type ClaimEscalationResult = {
66
+ ok: true;
67
+ entry: EscalationEntry;
68
+ } | {
69
+ ok: false;
70
+ reason: 'not-found' | 'conflict';
71
+ };
72
+ /**
73
+ * Result of `claimByMetadata()`. Includes `candidatesExist` — the total
74
+ * count of rows matching the metadata filter regardless of claimability — so
75
+ * callers can distinguish "nothing matching at all" from "found candidates but
76
+ * all are locked or in-progress".
77
+ */
78
+ export type ClaimByMetadataResult = {
79
+ ok: true;
80
+ entry: EscalationEntry;
81
+ candidatesExist: number;
82
+ } | {
83
+ ok: false;
84
+ reason: 'not-found' | 'conflict';
85
+ candidatesExist: number;
86
+ };
87
+ export type ResolveEscalationResult = {
88
+ ok: true;
89
+ } | {
90
+ ok: false;
91
+ reason: 'not-found' | 'already-resolved' | 'already-cancelled' | 'signal-failed';
92
+ };
93
+ export type ReleaseEscalationResult = {
94
+ ok: true;
95
+ } | {
96
+ ok: false;
97
+ reason: 'not-found' | 'wrong-assignee';
98
+ };
99
+ export type CancelEscalationResult = {
100
+ ok: true;
101
+ } | {
102
+ ok: false;
103
+ reason: 'not-found' | 'already-terminal';
104
+ };
105
+ export interface ListEscalationsParams {
106
+ namespace?: string;
107
+ role?: string;
108
+ type?: string;
109
+ subtype?: string;
110
+ entity?: string;
111
+ status?: string;
112
+ assignedTo?: string;
113
+ workflowId?: string;
114
+ originId?: string;
115
+ limit?: number;
116
+ offset?: number;
117
+ }
118
+ export interface CreateEscalationParams {
119
+ namespace?: string;
120
+ appId?: string;
121
+ signalKey?: string;
122
+ topic?: string;
123
+ workflowId?: string;
124
+ taskQueue?: string;
125
+ workflowType?: string;
126
+ type?: string;
127
+ subtype?: string;
128
+ entity?: string;
129
+ description?: string;
130
+ role?: string;
131
+ priority?: number;
132
+ originId?: string;
133
+ parentId?: string;
134
+ initiatedBy?: string;
135
+ createdBy?: string;
136
+ traceId?: string;
137
+ spanId?: string;
138
+ escalationPayload?: Record<string, unknown>;
139
+ metadata?: Record<string, unknown>;
140
+ envelope?: Record<string, unknown>;
141
+ expiresAt?: Date;
142
+ }
143
+ /**
144
+ * Fields that can be patched on an existing escalation. All fields are
145
+ * optional — only provided fields are written. Signal routing fields
146
+ * (`signalKey`, `topic`, `workflowId`, `taskQueue`, `workflowType`) support
147
+ * the legacy two-step pattern where routing context is enriched after creation.
148
+ */
149
+ export interface UpdateEscalationParams {
150
+ id: string;
151
+ namespace?: string;
152
+ description?: string;
153
+ priority?: number;
154
+ role?: string;
155
+ /** Merged into existing metadata (keys overwritten, others preserved) */
156
+ metadata?: Record<string, unknown>;
157
+ /** Replaces existing envelope */
158
+ envelope?: Record<string, unknown>;
159
+ /** Signal routing enrichment — equivalent to long-tail's enrichEscalationRouting */
160
+ signalKey?: string;
161
+ topic?: string;
162
+ workflowId?: string;
163
+ taskQueue?: string;
164
+ workflowType?: string;
165
+ expiresAt?: Date;
166
+ }
167
+ export interface AppendMilestonesParams {
168
+ id: string;
169
+ namespace?: string;
170
+ milestones: Array<{
171
+ name: string;
172
+ value: unknown;
173
+ [key: string]: unknown;
174
+ }>;
175
+ }
176
+ export interface ClaimEscalationParams {
177
+ id: string;
178
+ namespace?: string;
179
+ assignee?: string;
180
+ durationMinutes?: number;
181
+ }
182
+ export interface ClaimByMetadataParams {
183
+ key: string;
184
+ value: unknown;
185
+ namespace?: string;
186
+ assignee?: string;
187
+ durationMinutes?: number;
188
+ roles?: string[];
189
+ }
190
+ export interface ReleaseEscalationParams {
191
+ id: string;
192
+ namespace?: string;
193
+ /** When provided, the release is rejected with `wrong-assignee` if the current assignee differs */
194
+ assignee?: string;
195
+ }
196
+ export interface ResolveEscalationParams {
197
+ id: string;
198
+ namespace?: string;
199
+ resolverPayload?: Record<string, unknown>;
200
+ }
201
+ export interface ResolveByMetadataParams {
202
+ key: string;
203
+ value: unknown;
204
+ namespace?: string;
205
+ resolverPayload?: Record<string, unknown>;
206
+ roles?: string[];
207
+ }
208
+ export interface EscalateToRoleParams {
209
+ id: string;
210
+ targetRole: string;
211
+ namespace?: string;
212
+ }
213
+ /**
214
+ * Full-fidelity migration params. Extends `CreateEscalationParams` with:
215
+ * - `id` (required) — preserves the original UUID; no auto-generation
216
+ * - lifecycle state fields (`status`, `assignedTo`, `claimExpiresAt`, …) — carry over
217
+ * the exact state of the migrated row so in-flight escalations land correctly
218
+ * - `createdAt` / `updatedAt` — preserve original timestamps
219
+ *
220
+ * The underlying INSERT uses `ON CONFLICT (id) DO NOTHING`, so calling
221
+ * `migrate()` multiple times with the same ID is safe — subsequent calls
222
+ * return `null` without touching the existing row.
223
+ */
224
+ export interface MigrateEscalationParams extends CreateEscalationParams {
225
+ /** Required — preserve the original UUID from the source table. */
226
+ id: string;
227
+ status?: 'pending' | 'claimed' | 'resolved' | 'cancelled' | 'expired';
228
+ assignedTo?: string;
229
+ claimExpiresAt?: Date;
230
+ claimedAt?: Date;
231
+ resolvedAt?: Date;
232
+ resolverPayload?: Record<string, unknown>;
233
+ milestones?: Array<{
234
+ name: string;
235
+ value: unknown;
236
+ [key: string]: unknown;
237
+ }>;
238
+ createdAt?: Date;
239
+ updatedAt?: Date;
240
+ }
@@ -25,4 +25,4 @@ export { ReclaimedMessageType, RetryPolicy, RouterConfig, StreamCode, StreamConf
25
25
  export { context, Context, Counter, Meter, metrics, propagation, SpanContext, Span, SpanStatus, SpanStatusCode, SpanKind, trace, Tracer, ValueType, } from './telemetry';
26
26
  export { WorkListTaskType } from './task';
27
27
  export { TransitionMatch, TransitionRule, Transitions } from './transition';
28
- export { ClaimSignalByMetadataParams, ClaimSignalParams, ClaimSignalResult, ConditionQueueConfig, EnqueueSignalParams, ListSignalsParams, ReleaseSignalResult, ResolveSignalByMetadataParams, ResolveSignalParams, ResolveSignalResult, SignalQueueEntry, } from './signal';
28
+ export { ConditionQueueConfig, EscalationEntry, ClaimEscalationResult, ClaimByMetadataResult, ReleaseEscalationResult, ResolveEscalationResult, CancelEscalationResult, ListEscalationsParams, CreateEscalationParams, UpdateEscalationParams, AppendMilestonesParams, ClaimEscalationParams, ClaimByMetadataParams, ReleaseEscalationParams, ResolveEscalationParams, ResolveByMetadataParams, EscalateToRoleParams, MigrateEscalationParams, } from './hmsh_escalations';
@@ -101,6 +101,8 @@ export interface HSetOptions {
101
101
  nx?: boolean;
102
102
  ex?: number;
103
103
  entity?: string;
104
+ originId?: string;
105
+ parentId?: string;
104
106
  }
105
107
  export interface ZAddOptions {
106
108
  nx?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.21.1",
3
+ "version": "0.22.1",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -50,6 +50,7 @@
50
50
  "test:durable:signal": "vitest run tests/durable/signal/postgres.test.ts",
51
51
  "test:durable:readonly": "docker compose --profile readonly up -d --build && docker compose exec hotmesh-readonly npx vitest run --config tests/durable/readonly/vitest.config.mts",
52
52
  "test:durable:unknown": "vitest run tests/durable/unknown/postgres.test.ts",
53
+ "test:durable:escalations": "HMSH_LOGLEVEL=info vitest run tests/durable/escalations",
53
54
  "test:durable:exporter": "HMSH_LOGLEVEL=info vitest run tests/durable/exporter",
54
55
  "test:durable:exporter:debug": "EXPORT_DEBUG=1 HMSH_LOGLEVEL=error vitest run tests/durable/basic/postgres.test.ts",
55
56
  "test:durable:codec": "vitest run tests/durable/codec/postgres.test.ts",
@@ -84,7 +85,13 @@
84
85
  "test:sub:nats": "vitest run tests/functional/sub/providers/nats/nats.test.ts",
85
86
  "test:trigger": "vitest run tests/unit/services/activities/trigger.test.ts",
86
87
  "test:virtual": "vitest run tests/virtual",
87
- "test:unit": "vitest run tests/unit"
88
+ "test:unit": "vitest run tests/unit",
89
+
90
+ "prove": "docker compose exec hotmesh npx vitest run tests/durable 2>&1 | tee /tmp/hmsh-durable.txt && grep -E 'FAIL|Tests |Files ' /tmp/hmsh-durable.txt | tail -5",
91
+ "prove:escalations": "docker compose exec hotmesh npx vitest run tests/durable/escalations/postgres.test.ts 2>&1 | tee /tmp/hmsh-escalations.txt && grep -E 'FAIL|Tests |Files ' /tmp/hmsh-escalations.txt | tail -5",
92
+ "prove:functional": "docker compose exec hotmesh npx vitest run tests/functional 2>&1 | tee /tmp/hmsh-functional.txt && grep -E 'FAIL|Tests |Files ' /tmp/hmsh-functional.txt | tail -5",
93
+ "prove:all": "docker compose exec hotmesh npx vitest run tests/ 2>&1 | tee /tmp/hmsh-all.txt && grep -E 'FAIL|Tests |Files ' /tmp/hmsh-all.txt | tail -5",
94
+ "prove:file": "f() { docker compose exec hotmesh npx vitest run \"$@\" 2>&1 | tee /tmp/hmsh-file.txt && grep -E 'FAIL|Tests |Files ' /tmp/hmsh-file.txt | tail -5; }; f"
88
95
  },
89
96
  "keywords": [
90
97
  "Invisible Infrastructure",
@@ -1,147 +0,0 @@
1
- /**
2
- * Represents a queued signal record in the hotmesh_signals table.
3
- * Created atomically with workflow suspension when condition() is called
4
- * with a queue configuration.
5
- */
6
- export interface SignalQueueEntry {
7
- id: string;
8
- namespace: string;
9
- appId: string;
10
- signalKey: string;
11
- workflowId: string;
12
- jobId?: string;
13
- topic?: string;
14
- status: 'pending' | 'claimed' | 'resolved' | 'expired' | 'released';
15
- role?: string;
16
- type?: string;
17
- subtype?: string;
18
- priority: number;
19
- description?: string;
20
- taskQueue?: string;
21
- workflowType?: string;
22
- assignedTo?: string;
23
- claimedAt?: Date;
24
- claimExpiresAt?: Date;
25
- resolvedAt?: Date;
26
- resolverPayload?: Record<string, unknown>;
27
- envelope?: Record<string, unknown>;
28
- metadata?: Record<string, unknown>;
29
- expiresAt?: Date;
30
- createdAt: Date;
31
- updatedAt: Date;
32
- }
33
- /**
34
- * Optional queue configuration passed to condition() to create a
35
- * richly-typed, indexed, claimable signal record alongside workflow suspension.
36
- *
37
- * metadata is GIN-indexed and used for claimByMetadata / resolveByMetadata queries.
38
- * envelope is unindexed and intended for display context (form schemas, etc.).
39
- */
40
- export interface ConditionQueueConfig {
41
- role?: string;
42
- type?: string;
43
- subtype?: string;
44
- priority?: number;
45
- description?: string;
46
- taskQueue?: string;
47
- workflowType?: string;
48
- assignedTo?: string;
49
- metadata?: Record<string, unknown>;
50
- envelope?: Record<string, unknown>;
51
- durationMinutes?: number;
52
- }
53
- export interface EnqueueSignalParams {
54
- namespace: string;
55
- appId: string;
56
- signalKey: string;
57
- workflowId: string;
58
- jobId?: string;
59
- topic?: string;
60
- role?: string;
61
- type?: string;
62
- subtype?: string;
63
- priority?: number;
64
- description?: string;
65
- taskQueue?: string;
66
- workflowType?: string;
67
- assignedTo?: string;
68
- metadata?: Record<string, unknown>;
69
- envelope?: Record<string, unknown>;
70
- expiresAt?: Date;
71
- }
72
- export interface ClaimSignalParams {
73
- id: string;
74
- assignee?: string;
75
- durationMinutes?: number;
76
- }
77
- export interface ClaimSignalByMetadataParams {
78
- key: string;
79
- value: unknown;
80
- assignee?: string;
81
- durationMinutes?: number;
82
- }
83
- export interface ResolveSignalParams {
84
- id: string;
85
- resolverPayload?: Record<string, unknown>;
86
- }
87
- export interface ResolveSignalByMetadataParams {
88
- key: string;
89
- value: unknown;
90
- resolverPayload?: Record<string, unknown>;
91
- }
92
- export interface ListSignalsParams {
93
- status?: 'pending' | 'claimed' | 'resolved' | 'expired' | 'released';
94
- role?: string;
95
- taskQueue?: string;
96
- limit?: number;
97
- offset?: number;
98
- }
99
- /**
100
- * Result of a claim operation (by ID or by metadata).
101
- *
102
- * - ok: true → signal was claimed; entry contains the full record
103
- * - ok: false, reason: 'not-found' → no signal exists for the given id/metadata
104
- * - ok: false, reason: 'conflict' → signal exists but was already claimed concurrently
105
- */
106
- export type ClaimSignalResult = {
107
- ok: true;
108
- entry: SignalQueueEntry;
109
- } | {
110
- ok: false;
111
- reason: 'not-found' | 'conflict';
112
- };
113
- /**
114
- * Result of a resolve operation (by ID or by metadata).
115
- *
116
- * - ok: true → signal marked resolved and workflow signal delivered
117
- * - ok: false, reason: 'not-found' → no pending/claimed signal found
118
- * - ok: false, reason: 'already-resolved' → signal exists but was already resolved (id-based only)
119
- * - ok: false, reason: 'signal-failed' → DB record updated but workflow signal delivery failed;
120
- * signalKey is provided so callers can retry delivery
121
- */
122
- export type ResolveSignalResult = {
123
- ok: true;
124
- } | {
125
- ok: false;
126
- reason: 'not-found';
127
- } | {
128
- ok: false;
129
- reason: 'already-resolved';
130
- } | {
131
- ok: false;
132
- reason: 'signal-failed';
133
- signalKey: string;
134
- };
135
- /**
136
- * Result of a release operation.
137
- *
138
- * - ok: true → signal returned to pending
139
- * - ok: false, reason: 'not-found' → no signal exists for this id
140
- * - ok: false, reason: 'wrong-status' → signal exists but is not in 'claimed' status
141
- */
142
- export type ReleaseSignalResult = {
143
- ok: true;
144
- } | {
145
- ok: false;
146
- reason: 'not-found' | 'wrong-status';
147
- };
File without changes