@hotmeshio/hotmesh 0.22.0 → 0.22.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +2 -1
- package/build/index.js +3 -1
- package/build/modules/utils.d.ts +2 -0
- package/build/modules/utils.js +6 -1
- package/build/package.json +8 -3
- package/build/services/durable/client.d.ts +8 -238
- package/build/services/durable/client.js +5 -335
- package/build/services/durable/index.d.ts +7 -1
- package/build/services/durable/index.js +6 -0
- package/build/services/escalations/client.d.ts +130 -0
- package/build/services/escalations/client.js +262 -0
- package/build/services/escalations/index.d.ts +66 -0
- package/build/services/escalations/index.js +71 -0
- package/build/services/store/providers/postgres/postgres.d.ts +13 -5
- package/build/services/store/providers/postgres/postgres.js +172 -40
- package/build/types/hmsh_escalations.d.ts +43 -6
- package/build/types/index.d.ts +1 -1
- package/index.ts +2 -0
- package/package.json +9 -3
|
@@ -7,6 +7,7 @@ const utils_1 = require("../../modules/utils");
|
|
|
7
7
|
const hotmesh_1 = require("../hotmesh");
|
|
8
8
|
const key_1 = require("../../modules/key");
|
|
9
9
|
const types_1 = require("../../types");
|
|
10
|
+
const client_1 = require("../escalations/client");
|
|
10
11
|
const search_1 = require("./search");
|
|
11
12
|
const handle_1 = require("./handle");
|
|
12
13
|
const factory_1 = require("./schemas/factory");
|
|
@@ -284,342 +285,11 @@ class ClientService {
|
|
|
284
285
|
}
|
|
285
286
|
},
|
|
286
287
|
};
|
|
287
|
-
/**
|
|
288
|
-
* Escalation queue operations over `public.hmsh_escalations` — a global
|
|
289
|
-
* table that surfaces workflow signal pauses as role-based, claimable,
|
|
290
|
-
* searchable queue items.
|
|
291
|
-
*
|
|
292
|
-
* When a YAML `hook` activity suspends with an `escalation:` block, or
|
|
293
|
-
* `Durable.workflow.condition(signalId, config)` fires, **one row is
|
|
294
|
-
* written atomically** with the workflow checkpoint — no enrichment step,
|
|
295
|
-
* no secondary round-trip. Every connected app shares the same table;
|
|
296
|
-
* rows are namespaced by `namespace` + `app_id`.
|
|
297
|
-
*
|
|
298
|
-
* **Status lifecycle:**
|
|
299
|
-
* ```
|
|
300
|
-
* pending → claimed → resolved
|
|
301
|
-
* ↘ cancelled (any non-terminal state)
|
|
302
|
-
* ↗ pending (via release or releaseExpired)
|
|
303
|
-
* ```
|
|
304
|
-
*
|
|
305
|
-
* **Typical human-in-the-loop flow:**
|
|
306
|
-
* ```typescript
|
|
307
|
-
* // 1. Workflow pauses and writes the escalation row automatically
|
|
308
|
-
* const decision = await Durable.workflow.condition('manager-approval', {
|
|
309
|
-
* role: 'manager',
|
|
310
|
-
* type: 'order-approval',
|
|
311
|
-
* priority: 2,
|
|
312
|
-
* metadata: { orderId },
|
|
313
|
-
* });
|
|
314
|
-
*
|
|
315
|
-
* // 2. Dashboard lists pending approvals for this role
|
|
316
|
-
* const [item] = await client.escalations.list({ role: 'manager', status: 'pending' });
|
|
317
|
-
*
|
|
318
|
-
* // 3. Reviewer claims it (sets assigned_to + expiry)
|
|
319
|
-
* await client.escalations.claim({ id: item.id, assignee: 'alice@company.com' });
|
|
320
|
-
*
|
|
321
|
-
* // 4. Resolve atomically marks it resolved AND delivers the signal
|
|
322
|
-
* await client.escalations.resolve({
|
|
323
|
-
* id: item.id,
|
|
324
|
-
* resolverPayload: { approved: true },
|
|
325
|
-
* });
|
|
326
|
-
* // workflow resumes with { approved: true }
|
|
327
|
-
* ```
|
|
328
|
-
*/
|
|
329
|
-
this.escalations = {
|
|
330
|
-
/**
|
|
331
|
-
* Returns all escalation rows matching the given filters.
|
|
332
|
-
*
|
|
333
|
-
* @example
|
|
334
|
-
* ```typescript
|
|
335
|
-
* // All pending approvals for the manager role
|
|
336
|
-
* const items = await client.escalations.list({ role: 'manager', status: 'pending' });
|
|
337
|
-
*
|
|
338
|
-
* // By workflow ID
|
|
339
|
-
* const items = await client.escalations.list({ workflowId: 'order-123' });
|
|
340
|
-
* ```
|
|
341
|
-
*/
|
|
342
|
-
list: async (params) => {
|
|
343
|
-
const hotMeshClient = await this.getHotMeshClient(null, params?.namespace);
|
|
344
|
-
return hotMeshClient.engine.store.listEscalations(params ?? {});
|
|
345
|
-
},
|
|
346
|
-
/**
|
|
347
|
-
* Returns a single escalation row by its UUID primary key.
|
|
348
|
-
* Returns `null` if not found.
|
|
349
|
-
*/
|
|
350
|
-
get: async (id, namespace) => {
|
|
351
|
-
const hotMeshClient = await this.getHotMeshClient(null, namespace);
|
|
352
|
-
return hotMeshClient.engine.store.getEscalation(id, namespace);
|
|
353
|
-
},
|
|
354
|
-
/**
|
|
355
|
-
* Looks up an escalation row by its `signal_key` — the value that was
|
|
356
|
-
* passed to `condition()` or stored in the hook activity's collation rule.
|
|
357
|
-
* This is the same key used to deliver the signal via `hotMesh.signal()`.
|
|
358
|
-
*
|
|
359
|
-
* @example
|
|
360
|
-
* ```typescript
|
|
361
|
-
* const item = await client.escalations.getBySignalKey('manager-approval');
|
|
362
|
-
* ```
|
|
363
|
-
*/
|
|
364
|
-
getBySignalKey: async (signalKey, namespace) => {
|
|
365
|
-
const hotMeshClient = await this.getHotMeshClient(null, namespace);
|
|
366
|
-
return hotMeshClient.engine.store.getEscalationBySignalKey(signalKey, namespace);
|
|
367
|
-
},
|
|
368
|
-
/**
|
|
369
|
-
* Creates a standalone escalation row that is **not** backed by a signal.
|
|
370
|
-
* `signal_key` is `null`. Useful for external task tracking that doesn't
|
|
371
|
-
* need to resume a workflow (e.g., audit tasks, out-of-band approvals).
|
|
372
|
-
*
|
|
373
|
-
* @example
|
|
374
|
-
* ```typescript
|
|
375
|
-
* const entry = await client.escalations.create({
|
|
376
|
-
* role: 'support',
|
|
377
|
-
* type: 'data-correction',
|
|
378
|
-
* description: 'Fix the customer address',
|
|
379
|
-
* metadata: { customerId: 'cust-42' },
|
|
380
|
-
* });
|
|
381
|
-
* ```
|
|
382
|
-
*/
|
|
383
|
-
create: async (params) => {
|
|
384
|
-
const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
|
|
385
|
-
return hotMeshClient.engine.store.createEscalation(params);
|
|
386
|
-
},
|
|
387
|
-
/**
|
|
388
|
-
* Patches an existing escalation row. All fields are optional — only
|
|
389
|
-
* provided fields are written. `metadata` is **merged**, not replaced.
|
|
390
|
-
*
|
|
391
|
-
* Signal routing fields (`signalKey`, `topic`, `workflowId`, …) can be
|
|
392
|
-
* enriched after the row is created — useful when the row is created
|
|
393
|
-
* before the workflow starts and routing context is not yet known.
|
|
394
|
-
*
|
|
395
|
-
* @example
|
|
396
|
-
* ```typescript
|
|
397
|
-
* await client.escalations.update({
|
|
398
|
-
* id: item.id,
|
|
399
|
-
* description: 'Updated description',
|
|
400
|
-
* metadata: { extraKey: 'value' }, // merged into existing metadata
|
|
401
|
-
* });
|
|
402
|
-
* ```
|
|
403
|
-
*/
|
|
404
|
-
update: async (params) => {
|
|
405
|
-
const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
|
|
406
|
-
return hotMeshClient.engine.store.updateEscalation(params);
|
|
407
|
-
},
|
|
408
|
-
/**
|
|
409
|
-
* Appends one or more milestone entries to the escalation's
|
|
410
|
-
* `milestones` audit trail array. Milestones are append-only; they
|
|
411
|
-
* record events like state transitions, reviewer notes, or external
|
|
412
|
-
* system callbacks.
|
|
413
|
-
*
|
|
414
|
-
* @example
|
|
415
|
-
* ```typescript
|
|
416
|
-
* await client.escalations.appendMilestones({
|
|
417
|
-
* id: item.id,
|
|
418
|
-
* milestones: [{ at: new Date().toISOString(), by: 'alice', note: 'Reviewed' }],
|
|
419
|
-
* });
|
|
420
|
-
* ```
|
|
421
|
-
*/
|
|
422
|
-
appendMilestones: async (params) => {
|
|
423
|
-
const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
|
|
424
|
-
return hotMeshClient.engine.store.appendEscalationMilestones(params);
|
|
425
|
-
},
|
|
426
|
-
/**
|
|
427
|
-
* Atomically claims an escalation row by UUID. Sets `assigned_to`,
|
|
428
|
-
* `claimed_at`, and `claim_expires_at`. Returns `conflict` if another
|
|
429
|
-
* actor already holds the claim.
|
|
430
|
-
*
|
|
431
|
-
* @example
|
|
432
|
-
* ```typescript
|
|
433
|
-
* const result = await client.escalations.claim({
|
|
434
|
-
* id: item.id,
|
|
435
|
-
* assignee: 'alice@company.com',
|
|
436
|
-
* durationMinutes: 30,
|
|
437
|
-
* });
|
|
438
|
-
* if (!result.ok) console.warn('Already claimed by someone else');
|
|
439
|
-
* ```
|
|
440
|
-
*/
|
|
441
|
-
claim: async (params) => {
|
|
442
|
-
const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
|
|
443
|
-
return hotMeshClient.engine.store.claimEscalation(params);
|
|
444
|
-
},
|
|
445
|
-
/**
|
|
446
|
-
* Atomically claims the highest-priority pending escalation whose
|
|
447
|
-
* `metadata` contains the given key/value pair. Uses
|
|
448
|
-
* `FOR UPDATE SKIP LOCKED` so concurrent callers never double-claim.
|
|
449
|
-
*
|
|
450
|
-
* Returns `candidatesExist` to distinguish two cases:
|
|
451
|
-
* - `not-found, candidatesExist: 0` — no rows matched the metadata filter
|
|
452
|
-
* - `conflict, candidatesExist: N` — matching rows exist but all are claimed
|
|
453
|
-
*
|
|
454
|
-
* @example
|
|
455
|
-
* ```typescript
|
|
456
|
-
* const result = await client.escalations.claimByMetadata({
|
|
457
|
-
* key: 'region',
|
|
458
|
-
* value: 'west',
|
|
459
|
-
* assignee: 'bob@company.com',
|
|
460
|
-
* roles: ['manager'],
|
|
461
|
-
* });
|
|
462
|
-
* if (result.ok) console.log('Claimed:', result.entry.id);
|
|
463
|
-
* ```
|
|
464
|
-
*/
|
|
465
|
-
claimByMetadata: async (params) => {
|
|
466
|
-
const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
|
|
467
|
-
return hotMeshClient.engine.store.claimEscalationByMetadata(params);
|
|
468
|
-
},
|
|
469
|
-
/**
|
|
470
|
-
* Releases a claimed escalation, returning it to `pending` status and
|
|
471
|
-
* clearing `assigned_to` and `claim_expires_at`. The row is immediately
|
|
472
|
-
* available for other actors to claim.
|
|
473
|
-
*
|
|
474
|
-
* @example
|
|
475
|
-
* ```typescript
|
|
476
|
-
* await client.escalations.release({ id: item.id });
|
|
477
|
-
* ```
|
|
478
|
-
*/
|
|
479
|
-
release: async (params) => {
|
|
480
|
-
const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
|
|
481
|
-
return hotMeshClient.engine.store.releaseEscalation(params);
|
|
482
|
-
},
|
|
483
|
-
/**
|
|
484
|
-
* Reassigns the escalation to a different role, clearing any current
|
|
485
|
-
* claim and returning status to `pending`. Use when an escalation must
|
|
486
|
-
* be handled by a different team or tier.
|
|
487
|
-
*
|
|
488
|
-
* @example
|
|
489
|
-
* ```typescript
|
|
490
|
-
* await client.escalations.escalateToRole({ id: item.id, role: 'senior-manager' });
|
|
491
|
-
* ```
|
|
492
|
-
*/
|
|
493
|
-
escalateToRole: async (params) => {
|
|
494
|
-
const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
|
|
495
|
-
return hotMeshClient.engine.store.escalateEscalationToRole(params);
|
|
496
|
-
},
|
|
497
|
-
/**
|
|
498
|
-
* Terminates the escalation without delivering a signal. Rows in
|
|
499
|
-
* `pending` or `claimed` state move to `cancelled`. Terminal rows
|
|
500
|
-
* (`resolved`, `cancelled`) return `already-terminal`.
|
|
501
|
-
*
|
|
502
|
-
* @example
|
|
503
|
-
* ```typescript
|
|
504
|
-
* await client.escalations.cancel(item.id);
|
|
505
|
-
* ```
|
|
506
|
-
*/
|
|
507
|
-
cancel: async (id, namespace) => {
|
|
508
|
-
const hotMeshClient = await this.getHotMeshClient(null, namespace);
|
|
509
|
-
return hotMeshClient.engine.store.cancelEscalation(id, namespace);
|
|
510
|
-
},
|
|
511
|
-
/**
|
|
512
|
-
* Atomically marks the escalation `resolved` **and** delivers the
|
|
513
|
-
* signal to the waiting workflow — one round-trip, no separate
|
|
514
|
-
* `signal()` call required. If `signal_key` is null (standalone
|
|
515
|
-
* escalation), only the row is updated.
|
|
516
|
-
*
|
|
517
|
-
* @example
|
|
518
|
-
* ```typescript
|
|
519
|
-
* const result = await client.escalations.resolve({
|
|
520
|
-
* id: item.id,
|
|
521
|
-
* resolverPayload: { approved: true, note: 'LGTM' },
|
|
522
|
-
* });
|
|
523
|
-
* if (!result.ok) console.error(result.reason); // 'not-found' | 'already-resolved' | 'signal-failed'
|
|
524
|
-
* // workflow resumes with { approved: true, note: 'LGTM' }
|
|
525
|
-
* ```
|
|
526
|
-
*/
|
|
527
|
-
resolve: async (params, namespace) => {
|
|
528
|
-
const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
|
|
529
|
-
const hotMeshClient = await this.getHotMeshClient(null, ns);
|
|
530
|
-
const store = hotMeshClient.engine.store;
|
|
531
|
-
// UUID primary-key lookup — no namespace filter needed; use existing.namespace for the UPDATE.
|
|
532
|
-
const existing = await store.getEscalation(params.id);
|
|
533
|
-
if (!existing)
|
|
534
|
-
return { ok: false, reason: 'not-found' };
|
|
535
|
-
if (existing.status === 'resolved')
|
|
536
|
-
return { ok: false, reason: 'already-resolved' };
|
|
537
|
-
if (existing.status === 'cancelled')
|
|
538
|
-
return { ok: false, reason: 'already-cancelled' };
|
|
539
|
-
// Build a single atomic transaction: signal stream INSERTs + escalation UPDATE in one BEGIN/COMMIT.
|
|
540
|
-
// engine.signal() with a transaction queues an INSERT into the stream table without executing;
|
|
541
|
-
// queueResolveEscalation() queues the status UPDATE; txn.exec() commits all in one round-trip.
|
|
542
|
-
const txn = store.transact();
|
|
543
|
-
if (existing.signal_key && existing.topic) {
|
|
544
|
-
const signalPayload = { id: existing.signal_key, data: params.resolverPayload ?? {} };
|
|
545
|
-
try {
|
|
546
|
-
const sc = await this.getHotMeshClient(`${ns}.wfs.signal`, ns);
|
|
547
|
-
await sc.engine.signal(`${ns}.wfs.signal`, signalPayload, types_1.StreamStatus.SUCCESS, 200, txn);
|
|
548
|
-
}
|
|
549
|
-
catch { /* no collator hook rule — skip */ }
|
|
550
|
-
try {
|
|
551
|
-
const wc = await this.getHotMeshClient(`${ns}.wfs.wait`, ns);
|
|
552
|
-
await wc.engine.signal(`${ns}.wfs.wait`, signalPayload, types_1.StreamStatus.SUCCESS, 200, txn);
|
|
553
|
-
}
|
|
554
|
-
catch { /* no waiter hook rule — skip */ }
|
|
555
|
-
}
|
|
556
|
-
store.queueResolveEscalation({ id: params.id, namespace: existing.namespace ?? ns, resolverPayload: params.resolverPayload }, txn);
|
|
557
|
-
await txn.exec();
|
|
558
|
-
return { ok: true };
|
|
559
|
-
},
|
|
560
|
-
/**
|
|
561
|
-
* Resolves the highest-priority matching escalation by metadata filter,
|
|
562
|
-
* then delivers its signal. Identical semantics to `resolve()` but
|
|
563
|
-
* selects the target row by metadata key/value instead of UUID.
|
|
564
|
-
*
|
|
565
|
-
* @example
|
|
566
|
-
* ```typescript
|
|
567
|
-
* await client.escalations.resolveByMetadata({
|
|
568
|
-
* key: 'orderId',
|
|
569
|
-
* value: 'order-123',
|
|
570
|
-
* resolverPayload: { approved: true },
|
|
571
|
-
* });
|
|
572
|
-
* ```
|
|
573
|
-
*/
|
|
574
|
-
resolveByMetadata: async (params, namespace) => {
|
|
575
|
-
const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
|
|
576
|
-
const hotMeshClient = await this.getHotMeshClient(null, ns);
|
|
577
|
-
const store = hotMeshClient.engine.store;
|
|
578
|
-
// Metadata lookup: filter by namespace to scope correctly, but use existing.namespace for the UPDATE.
|
|
579
|
-
const existing = await store.findEscalationByMetadata(params.key, params.value, params.roles ?? null);
|
|
580
|
-
if (!existing)
|
|
581
|
-
return { ok: false, reason: 'not-found' };
|
|
582
|
-
if (existing.status === 'resolved')
|
|
583
|
-
return { ok: false, reason: 'already-resolved' };
|
|
584
|
-
if (existing.status === 'cancelled')
|
|
585
|
-
return { ok: false, reason: 'already-cancelled' };
|
|
586
|
-
// Same atomic pattern as resolve(): signal INSERTs + UPDATE in one transaction.
|
|
587
|
-
const txn = store.transact();
|
|
588
|
-
if (existing.signal_key && existing.topic) {
|
|
589
|
-
const signalPayload = { id: existing.signal_key, data: params.resolverPayload ?? {} };
|
|
590
|
-
try {
|
|
591
|
-
const sc = await this.getHotMeshClient(`${ns}.wfs.signal`, ns);
|
|
592
|
-
await sc.engine.signal(`${ns}.wfs.signal`, signalPayload, types_1.StreamStatus.SUCCESS, 200, txn);
|
|
593
|
-
}
|
|
594
|
-
catch { /* no collator hook rule — skip */ }
|
|
595
|
-
try {
|
|
596
|
-
const wc = await this.getHotMeshClient(`${ns}.wfs.wait`, ns);
|
|
597
|
-
await wc.engine.signal(`${ns}.wfs.wait`, signalPayload, types_1.StreamStatus.SUCCESS, 200, txn);
|
|
598
|
-
}
|
|
599
|
-
catch { /* no waiter hook rule — skip */ }
|
|
600
|
-
}
|
|
601
|
-
store.queueResolveEscalation({ id: existing.id, namespace: existing.namespace ?? ns, resolverPayload: params.resolverPayload }, txn);
|
|
602
|
-
await txn.exec();
|
|
603
|
-
return { ok: true };
|
|
604
|
-
},
|
|
605
|
-
/**
|
|
606
|
-
* Releases all claimed escalations whose `claim_expires_at` has lapsed,
|
|
607
|
-
* returning them to `pending` so they can be claimed again. Returns the
|
|
608
|
-
* number of rows released. Call periodically from a maintenance job or
|
|
609
|
-
* cron to prevent stale claims from blocking the queue.
|
|
610
|
-
*
|
|
611
|
-
* @example
|
|
612
|
-
* ```typescript
|
|
613
|
-
* const released = await client.escalations.releaseExpired();
|
|
614
|
-
* console.log(`Released ${released} expired claims`);
|
|
615
|
-
* ```
|
|
616
|
-
*/
|
|
617
|
-
releaseExpired: async (namespace) => {
|
|
618
|
-
const hotMeshClient = await this.getHotMeshClient(null, namespace);
|
|
619
|
-
return hotMeshClient.engine.store.releaseExpiredEscalations(namespace);
|
|
620
|
-
},
|
|
621
|
-
};
|
|
622
288
|
this.connection = config.connection;
|
|
289
|
+
// Inject our getHotMeshClient so the escalation client shares the same engine pool.
|
|
290
|
+
this.escalations = new client_1.EscalationClientService({
|
|
291
|
+
getHotMeshClient: this.getHotMeshClient.bind(this),
|
|
292
|
+
});
|
|
623
293
|
}
|
|
624
294
|
hashOptions() {
|
|
625
295
|
if ('options' in this.connection) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HotMesh } from '../hotmesh';
|
|
2
2
|
import { ContextType, WorkflowInboundCallsInterceptor, WorkflowOutboundCallsInterceptor, ActivityInboundCallsInterceptor } from '../../types/durable';
|
|
3
|
-
import { guid } from '../../modules/utils';
|
|
3
|
+
import { guid, uuid } from '../../modules/utils';
|
|
4
4
|
import { ClientService } from './client';
|
|
5
5
|
import { ConnectionService } from './connection';
|
|
6
6
|
import { Search } from './search';
|
|
@@ -277,6 +277,12 @@ declare class DurableClass {
|
|
|
277
277
|
* Generate a unique identifier for workflow IDs
|
|
278
278
|
*/
|
|
279
279
|
static guid: typeof guid;
|
|
280
|
+
/**
|
|
281
|
+
* Generate a standard RFC 4122 v4 UUID — use for DB primary keys and
|
|
282
|
+
* any context that requires a hyphenated UUID format rather than the
|
|
283
|
+
* compact HotMesh guid format.
|
|
284
|
+
*/
|
|
285
|
+
static uuid: typeof uuid;
|
|
280
286
|
/**
|
|
281
287
|
* Provision a scoped Postgres role for a worker. The role can only
|
|
282
288
|
* dequeue, ack, and respond on its assigned stream names via stored
|
|
@@ -310,6 +310,12 @@ DurableClass.interceptorService = new interceptor_1.InterceptorService();
|
|
|
310
310
|
* Generate a unique identifier for workflow IDs
|
|
311
311
|
*/
|
|
312
312
|
DurableClass.guid = utils_1.guid;
|
|
313
|
+
/**
|
|
314
|
+
* Generate a standard RFC 4122 v4 UUID — use for DB primary keys and
|
|
315
|
+
* any context that requires a hyphenated UUID format rather than the
|
|
316
|
+
* compact HotMesh guid format.
|
|
317
|
+
*/
|
|
318
|
+
DurableClass.uuid = utils_1.uuid;
|
|
313
319
|
/**
|
|
314
320
|
* Provision a scoped Postgres role for a worker. The role can only
|
|
315
321
|
* dequeue, ack, and respond on its assigned stream names via stored
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { HotMesh } from '../hotmesh';
|
|
2
|
+
import { Connection } 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';
|
|
4
|
+
type GetHotMeshFn = (topic: string | null, namespace?: string) => Promise<HotMesh>;
|
|
5
|
+
export interface EscalationClientConfig {
|
|
6
|
+
/** Postgres connection options — used when creating a standalone EscalationClient. */
|
|
7
|
+
connection?: Connection;
|
|
8
|
+
/**
|
|
9
|
+
* Inject a pre-existing `getHotMeshClient` function (e.g. from Durable.Client).
|
|
10
|
+
* When provided, the client reuses the caller's engine pool — no second connection.
|
|
11
|
+
*/
|
|
12
|
+
getHotMeshClient?: GetHotMeshFn;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Standalone client for the `public.hmsh_escalations` signal-pause surface.
|
|
16
|
+
*
|
|
17
|
+
* Requires NO dependency on `services/durable/`. Any HotMesh consumer — AI
|
|
18
|
+
* agent, YAML DAG worker, REST API — can interact with the escalation queue
|
|
19
|
+
* directly with just a Postgres connection.
|
|
20
|
+
*
|
|
21
|
+
* Signal delivery (for `resolve()` / `resolveByMetadata()`) uses HotMesh's
|
|
22
|
+
* `engine.signal()` internally. The engine is initialised lazily on first use
|
|
23
|
+
* and cached for the lifetime of the process.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { Escalations } from '@hotmeshio/hotmesh';
|
|
28
|
+
* import { Client as Postgres } from 'pg';
|
|
29
|
+
*
|
|
30
|
+
* const client = new Escalations.Client({
|
|
31
|
+
* connection: {
|
|
32
|
+
* class: Postgres,
|
|
33
|
+
* options: { connectionString: 'postgresql://usr:pwd@localhost:5432/db' },
|
|
34
|
+
* },
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* // Claim the next available approval for the 'manager' role
|
|
38
|
+
* const result = await client.claimByMetadata({
|
|
39
|
+
* key: 'orderId', value: 'order-123',
|
|
40
|
+
* assignee: 'alice@company.com',
|
|
41
|
+
* roles: ['manager'],
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* if (result.ok) {
|
|
45
|
+
* await client.resolve({ id: result.entry.id, resolverPayload: { approved: true } });
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare class EscalationClientService {
|
|
50
|
+
private readonly _engine;
|
|
51
|
+
private readonly _connection?;
|
|
52
|
+
static instances: Map<string, HotMesh | Promise<HotMesh>>;
|
|
53
|
+
constructor(config?: EscalationClientConfig);
|
|
54
|
+
private _makeEngineFactory;
|
|
55
|
+
private _hashConnection;
|
|
56
|
+
private _deliverEscalationSignal;
|
|
57
|
+
/**
|
|
58
|
+
* Returns all escalation rows matching the given filters. Each row includes
|
|
59
|
+
* a computed `available` field (true = claimable). Supports `sortBy`,
|
|
60
|
+
* `sortOrder`, and multi-role `roles[]` filter.
|
|
61
|
+
*/
|
|
62
|
+
list(params?: ListEscalationsParams): Promise<EscalationEntry[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Returns the count of escalation rows matching the given filters.
|
|
65
|
+
* Uses the same filter parameters as `list()`.
|
|
66
|
+
*/
|
|
67
|
+
count(params?: ListEscalationsParams): Promise<number>;
|
|
68
|
+
/** Returns a single escalation row by UUID. Returns `null` if not found. */
|
|
69
|
+
get(id: string, namespace?: string): Promise<EscalationEntry | null>;
|
|
70
|
+
/** Looks up an escalation by `signal_key` — the value passed to `condition()`. */
|
|
71
|
+
getBySignalKey(signalKey: string, namespace?: string): Promise<EscalationEntry | null>;
|
|
72
|
+
/**
|
|
73
|
+
* Creates a standalone escalation row with `signal_key = null`.
|
|
74
|
+
* Useful for external task tracking that doesn't need to resume a workflow.
|
|
75
|
+
*/
|
|
76
|
+
create(params: CreateEscalationParams): Promise<EscalationEntry>;
|
|
77
|
+
/**
|
|
78
|
+
* Patches an existing escalation row. `metadata` is merged, not replaced.
|
|
79
|
+
* Signal routing fields can be enriched after creation.
|
|
80
|
+
*/
|
|
81
|
+
update(params: UpdateEscalationParams): Promise<EscalationEntry | null>;
|
|
82
|
+
/** Appends milestone entries to the escalation's audit trail. */
|
|
83
|
+
appendMilestones(params: AppendMilestonesParams): Promise<EscalationEntry | null>;
|
|
84
|
+
/**
|
|
85
|
+
* Atomically claims an escalation by UUID. Implicit model: `status` stays
|
|
86
|
+
* `'pending'`; claim is expressed via `assigned_to` + `assigned_until`.
|
|
87
|
+
*/
|
|
88
|
+
claim(params: ClaimEscalationParams): Promise<ClaimEscalationResult>;
|
|
89
|
+
/**
|
|
90
|
+
* Atomically claims the highest-priority pending escalation whose `metadata`
|
|
91
|
+
* contains the given key/value. Returns `isExtension: true` when the same
|
|
92
|
+
* assignee re-claims a row they already hold (extends the expiry).
|
|
93
|
+
*/
|
|
94
|
+
claimByMetadata(params: ClaimByMetadataParams): Promise<ClaimByMetadataResult>;
|
|
95
|
+
/** Releases a claimed escalation, returning it to available status. */
|
|
96
|
+
release(params: ReleaseEscalationParams): Promise<ReleaseEscalationResult>;
|
|
97
|
+
/**
|
|
98
|
+
* Reassigns the escalation to a different role, clearing any current claim
|
|
99
|
+
* and resetting status to `'pending'`.
|
|
100
|
+
*/
|
|
101
|
+
escalateToRole(params: EscalateToRoleParams): Promise<EscalationEntry | null>;
|
|
102
|
+
/**
|
|
103
|
+
* Cancels a pending escalation without delivering a signal. Terminal rows
|
|
104
|
+
* return `already-terminal`.
|
|
105
|
+
*/
|
|
106
|
+
cancel(id: string, namespace?: string): Promise<CancelEscalationResult>;
|
|
107
|
+
/**
|
|
108
|
+
* Signal-first resolve: marks the escalation resolved **and** delivers the
|
|
109
|
+
* signal to the waiting workflow in a single held transaction. If signal
|
|
110
|
+
* delivery fails, the transaction rolls back — `committed: false`.
|
|
111
|
+
*/
|
|
112
|
+
resolve(params: ResolveEscalationParams, namespace?: string): Promise<ResolveEscalationResult>;
|
|
113
|
+
/**
|
|
114
|
+
* Resolves the highest-priority matching escalation by metadata filter,
|
|
115
|
+
* then delivers its signal.
|
|
116
|
+
*/
|
|
117
|
+
resolveByMetadata(params: ResolveByMetadataParams, namespace?: string): Promise<ResolveEscalationResult>;
|
|
118
|
+
/**
|
|
119
|
+
* Full-fidelity migration: inserts an escalation row preserving the original
|
|
120
|
+
* UUID and lifecycle state. Returns `null` on duplicate (idempotent).
|
|
121
|
+
*/
|
|
122
|
+
migrate(params: MigrateEscalationParams, namespace?: string): Promise<EscalationEntry | null>;
|
|
123
|
+
/**
|
|
124
|
+
* No-op in the implicit claim model — availability is computed at query time
|
|
125
|
+
* from `assigned_until`. Kept for API compatibility.
|
|
126
|
+
*/
|
|
127
|
+
releaseExpired(namespace?: string): Promise<number>;
|
|
128
|
+
static shutdown(): Promise<void>;
|
|
129
|
+
}
|
|
130
|
+
export {};
|