@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
@@ -1,5 +1,6 @@
1
1
  import { HotMesh } from '../hotmesh';
2
2
  import { ClientConfig, ClientWorkflow, Connection, WorkflowOptions } from '../../types/durable';
3
+ import { EscalationEntry, ClaimEscalationResult, ClaimByMetadataResult, ReleaseEscalationResult, ResolveEscalationResult, CancelEscalationResult, ListEscalationsParams, CreateEscalationParams, UpdateEscalationParams, AppendMilestonesParams, ClaimEscalationParams, ClaimByMetadataParams, ReleaseEscalationParams, ResolveEscalationParams, ResolveByMetadataParams, EscalateToRoleParams, MigrateEscalationParams } from '../../types/hmsh_escalations';
3
4
  /**
4
5
  * Workflow client. Starts workflows, sends signals, and reads results.
5
6
  *
@@ -82,73 +83,6 @@ export declare class ClientService {
82
83
  * is accessed by calling workflow.start().
83
84
  */
84
85
  workflow: ClientWorkflow;
85
- /**
86
- * Signal queue API for managing paused-workflow task records.
87
- * Operations: list, get, claim, claimByMetadata, release, resolve,
88
- * resolveByMetadata, releaseExpired.
89
- *
90
- * Requires a Postgres store provider. Methods are no-ops (return null/false)
91
- * when called against a non-Postgres store.
92
- *
93
- * @example
94
- * ```typescript
95
- * // Claim a pending task by metadata key
96
- * const task = await client.signalQueue.claimByMetadata({
97
- * key: 'orderId', value: 'RX-123',
98
- * assignee: 'pharmacist-jane',
99
- * durationMinutes: 30,
100
- * });
101
- *
102
- * if (task) {
103
- * await client.signalQueue.resolve({
104
- * id: task.id,
105
- * resolverPayload: { approved: true },
106
- * });
107
- * // → paused workflow resumes with { approved: true }
108
- * }
109
- * ```
110
- */
111
- signalQueue: {
112
- list: (params?: {
113
- namespace?: string;
114
- taskQueue?: string;
115
- status?: 'pending' | 'claimed' | 'resolved' | 'expired' | 'released';
116
- role?: string;
117
- limit?: number;
118
- offset?: number;
119
- }) => Promise<any>;
120
- get: (id: string, namespace?: string) => Promise<any>;
121
- getBySignalKey: (signalKey: string, namespace?: string) => Promise<any>;
122
- claim: (params: {
123
- id: string;
124
- namespace?: string;
125
- assignee?: string;
126
- durationMinutes?: number;
127
- }) => Promise<import('../../types/signal').ClaimSignalResult>;
128
- claimByMetadata: (params: {
129
- key: string;
130
- value: unknown;
131
- namespace?: string;
132
- assignee?: string;
133
- durationMinutes?: number;
134
- }) => Promise<import('../../types/signal').ClaimSignalResult>;
135
- release: (params: {
136
- id: string;
137
- namespace?: string;
138
- }) => Promise<import('../../types/signal').ReleaseSignalResult>;
139
- resolve: (params: {
140
- id: string;
141
- namespace?: string;
142
- resolverPayload?: Record<string, unknown>;
143
- }) => Promise<import('../../types/signal').ResolveSignalResult>;
144
- resolveByMetadata: (params: {
145
- key: string;
146
- value: unknown;
147
- namespace?: string;
148
- resolverPayload?: Record<string, unknown>;
149
- }) => Promise<import('../../types/signal').ResolveSignalResult>;
150
- releaseExpired: (namespace?: string) => Promise<number>;
151
- };
152
86
  /**
153
87
  * Any router can be used to deploy and activate the HotMesh
154
88
  * distributed executable to the active quorum EXCEPT for
@@ -163,6 +97,278 @@ export declare class ClientService {
163
97
  * @private
164
98
  */
165
99
  activateWorkflow(hotMesh: HotMesh, appId?: string, version?: string): Promise<void>;
100
+ /**
101
+ * Escalation queue operations over `public.hmsh_escalations` — a global
102
+ * table that surfaces workflow signal pauses as role-based, claimable,
103
+ * searchable queue items.
104
+ *
105
+ * When a YAML `hook` activity suspends with an `escalation:` block, or
106
+ * `Durable.workflow.condition(signalId, config)` fires, **one row is
107
+ * written atomically** with the workflow checkpoint — no enrichment step,
108
+ * no secondary round-trip. Every connected app shares the same table;
109
+ * rows are namespaced by `namespace` + `app_id`.
110
+ *
111
+ * **Status lifecycle:**
112
+ * ```
113
+ * pending → claimed → resolved
114
+ * ↘ cancelled (any non-terminal state)
115
+ * ↗ pending (via release or releaseExpired)
116
+ * ```
117
+ *
118
+ * **Typical human-in-the-loop flow:**
119
+ * ```typescript
120
+ * // 1. Workflow pauses and writes the escalation row automatically
121
+ * const decision = await Durable.workflow.condition('manager-approval', {
122
+ * role: 'manager',
123
+ * type: 'order-approval',
124
+ * priority: 2,
125
+ * metadata: { orderId },
126
+ * });
127
+ *
128
+ * // 2. Dashboard lists pending approvals for this role
129
+ * const [item] = await client.escalations.list({ role: 'manager', status: 'pending' });
130
+ *
131
+ * // 3. Reviewer claims it (sets assigned_to + expiry)
132
+ * await client.escalations.claim({ id: item.id, assignee: 'alice@company.com' });
133
+ *
134
+ * // 4. Resolve atomically marks it resolved AND delivers the signal
135
+ * await client.escalations.resolve({
136
+ * id: item.id,
137
+ * resolverPayload: { approved: true },
138
+ * });
139
+ * // workflow resumes with { approved: true }
140
+ * ```
141
+ */
142
+ escalations: {
143
+ /**
144
+ * Returns all escalation rows matching the given filters.
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * // All pending approvals for the manager role
149
+ * const items = await client.escalations.list({ role: 'manager', status: 'pending' });
150
+ *
151
+ * // By workflow ID
152
+ * const items = await client.escalations.list({ workflowId: 'order-123' });
153
+ * ```
154
+ */
155
+ list: (params?: ListEscalationsParams) => Promise<EscalationEntry[]>;
156
+ /**
157
+ * Returns a single escalation row by its UUID primary key.
158
+ * Returns `null` if not found.
159
+ */
160
+ get: (id: string, namespace?: string) => Promise<EscalationEntry | null>;
161
+ /**
162
+ * Looks up an escalation row by its `signal_key` — the value that was
163
+ * passed to `condition()` or stored in the hook activity's collation rule.
164
+ * This is the same key used to deliver the signal via `hotMesh.signal()`.
165
+ *
166
+ * @example
167
+ * ```typescript
168
+ * const item = await client.escalations.getBySignalKey('manager-approval');
169
+ * ```
170
+ */
171
+ getBySignalKey: (signalKey: string, namespace?: string) => Promise<EscalationEntry | null>;
172
+ /**
173
+ * Creates a standalone escalation row that is **not** backed by a signal.
174
+ * `signal_key` is `null`. Useful for external task tracking that doesn't
175
+ * need to resume a workflow (e.g., audit tasks, out-of-band approvals).
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * const entry = await client.escalations.create({
180
+ * role: 'support',
181
+ * type: 'data-correction',
182
+ * description: 'Fix the customer address',
183
+ * metadata: { customerId: 'cust-42' },
184
+ * });
185
+ * ```
186
+ */
187
+ create: (params: CreateEscalationParams) => Promise<EscalationEntry>;
188
+ /**
189
+ * Patches an existing escalation row. All fields are optional — only
190
+ * provided fields are written. `metadata` is **merged**, not replaced.
191
+ *
192
+ * Signal routing fields (`signalKey`, `topic`, `workflowId`, …) can be
193
+ * enriched after the row is created — useful when the row is created
194
+ * before the workflow starts and routing context is not yet known.
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * await client.escalations.update({
199
+ * id: item.id,
200
+ * description: 'Updated description',
201
+ * metadata: { extraKey: 'value' }, // merged into existing metadata
202
+ * });
203
+ * ```
204
+ */
205
+ update: (params: UpdateEscalationParams) => Promise<EscalationEntry | null>;
206
+ /**
207
+ * Appends one or more milestone entries to the escalation's
208
+ * `milestones` audit trail array. Milestones are append-only; they
209
+ * record events like state transitions, reviewer notes, or external
210
+ * system callbacks.
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * await client.escalations.appendMilestones({
215
+ * id: item.id,
216
+ * milestones: [{ at: new Date().toISOString(), by: 'alice', note: 'Reviewed' }],
217
+ * });
218
+ * ```
219
+ */
220
+ appendMilestones: (params: AppendMilestonesParams) => Promise<EscalationEntry | null>;
221
+ /**
222
+ * Atomically claims an escalation row by UUID. Sets `assigned_to`,
223
+ * `claimed_at`, and `claim_expires_at`. Returns `conflict` if another
224
+ * actor already holds the claim.
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * const result = await client.escalations.claim({
229
+ * id: item.id,
230
+ * assignee: 'alice@company.com',
231
+ * durationMinutes: 30,
232
+ * });
233
+ * if (!result.ok) console.warn('Already claimed by someone else');
234
+ * ```
235
+ */
236
+ claim: (params: ClaimEscalationParams) => Promise<ClaimEscalationResult>;
237
+ /**
238
+ * Atomically claims the highest-priority pending escalation whose
239
+ * `metadata` contains the given key/value pair. Uses
240
+ * `FOR UPDATE SKIP LOCKED` so concurrent callers never double-claim.
241
+ *
242
+ * Returns `candidatesExist` to distinguish two cases:
243
+ * - `not-found, candidatesExist: 0` — no rows matched the metadata filter
244
+ * - `conflict, candidatesExist: N` — matching rows exist but all are claimed
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * const result = await client.escalations.claimByMetadata({
249
+ * key: 'region',
250
+ * value: 'west',
251
+ * assignee: 'bob@company.com',
252
+ * roles: ['manager'],
253
+ * });
254
+ * if (result.ok) console.log('Claimed:', result.entry.id);
255
+ * ```
256
+ */
257
+ claimByMetadata: (params: ClaimByMetadataParams) => Promise<ClaimByMetadataResult>;
258
+ /**
259
+ * Releases a claimed escalation, returning it to `pending` status and
260
+ * clearing `assigned_to` and `claim_expires_at`. The row is immediately
261
+ * available for other actors to claim.
262
+ *
263
+ * @example
264
+ * ```typescript
265
+ * await client.escalations.release({ id: item.id });
266
+ * ```
267
+ */
268
+ release: (params: ReleaseEscalationParams) => Promise<ReleaseEscalationResult>;
269
+ /**
270
+ * Reassigns the escalation to a different role, clearing any current
271
+ * claim and returning status to `pending`. Use when an escalation must
272
+ * be handled by a different team or tier.
273
+ *
274
+ * @example
275
+ * ```typescript
276
+ * await client.escalations.escalateToRole({ id: item.id, role: 'senior-manager' });
277
+ * ```
278
+ */
279
+ escalateToRole: (params: EscalateToRoleParams) => Promise<EscalationEntry | null>;
280
+ /**
281
+ * Terminates the escalation without delivering a signal. Rows in
282
+ * `pending` or `claimed` state move to `cancelled`. Terminal rows
283
+ * (`resolved`, `cancelled`) return `already-terminal`.
284
+ *
285
+ * @example
286
+ * ```typescript
287
+ * await client.escalations.cancel(item.id);
288
+ * ```
289
+ */
290
+ cancel: (id: string, namespace?: string) => Promise<CancelEscalationResult>;
291
+ /**
292
+ * Atomically marks the escalation `resolved` **and** delivers the
293
+ * signal to the waiting workflow — one round-trip, no separate
294
+ * `signal()` call required. If `signal_key` is null (standalone
295
+ * escalation), only the row is updated.
296
+ *
297
+ * @example
298
+ * ```typescript
299
+ * const result = await client.escalations.resolve({
300
+ * id: item.id,
301
+ * resolverPayload: { approved: true, note: 'LGTM' },
302
+ * });
303
+ * if (!result.ok) console.error(result.reason); // 'not-found' | 'already-resolved' | 'signal-failed'
304
+ * // workflow resumes with { approved: true, note: 'LGTM' }
305
+ * ```
306
+ */
307
+ resolve: (params: ResolveEscalationParams, namespace?: string) => Promise<ResolveEscalationResult>;
308
+ /**
309
+ * Resolves the highest-priority matching escalation by metadata filter,
310
+ * then delivers its signal. Identical semantics to `resolve()` but
311
+ * selects the target row by metadata key/value instead of UUID.
312
+ *
313
+ * @example
314
+ * ```typescript
315
+ * await client.escalations.resolveByMetadata({
316
+ * key: 'orderId',
317
+ * value: 'order-123',
318
+ * resolverPayload: { approved: true },
319
+ * });
320
+ * ```
321
+ */
322
+ resolveByMetadata: (params: ResolveByMetadataParams, namespace?: string) => Promise<ResolveEscalationResult>;
323
+ /**
324
+ * Full-fidelity migration: inserts an escalation row preserving the original
325
+ * UUID and all lifecycle state. Returns the inserted row, or `null` if the
326
+ * UUID already exists (idempotent — safe to call multiple times with the same
327
+ * `params.id`). Use this to migrate rows from a legacy escalation table to
328
+ * `hmsh_escalations` without losing original IDs or state.
329
+ *
330
+ * @example
331
+ * ```typescript
332
+ * const entry = await client.escalations.migrate({
333
+ * id: 'original-uuid',
334
+ * status: 'resolved',
335
+ * resolvedAt: new Date('2025-01-01'),
336
+ * type: 'order-approval',
337
+ * role: 'approver',
338
+ * });
339
+ * // null on subsequent calls with the same id — idempotent
340
+ * ```
341
+ */
342
+ migrate: (params: MigrateEscalationParams, namespace?: string) => Promise<EscalationEntry | null>;
343
+ /**
344
+ * Releases all claimed escalations whose `claim_expires_at` has lapsed,
345
+ * returning them to `pending` so they can be claimed again. Returns the
346
+ * number of rows released. Call periodically from a maintenance job or
347
+ * cron to prevent stale claims from blocking the queue.
348
+ *
349
+ * @example
350
+ * ```typescript
351
+ * const released = await client.escalations.releaseExpired();
352
+ * console.log(`Released ${released} expired claims`);
353
+ * ```
354
+ */
355
+ releaseExpired: (namespace?: string) => Promise<number>;
356
+ };
357
+ /**
358
+ * Delivers a signal to the registered escalation topic.
359
+ *
360
+ * When the topic is known (stored at condition() time), we deliver only to that
361
+ * topic. This avoids writing a stale pending entry to the alternate stream, which
362
+ * would interfere with concurrent consumers in other workflows or tests.
363
+ *
364
+ * When topic is null (legacy/standalone rows with no registered topic), we try
365
+ * both wfs.signal and wfs.wait unconditionally — engine.signal() on the wrong
366
+ * topic stores pending without throwing, so a sequential fallback would skip
367
+ * the second topic and leave single-condition workflows permanently suspended.
368
+ *
369
+ * @private
370
+ */
371
+ private _deliverEscalationSignal;
166
372
  /**
167
373
  * @private
168
374
  */