@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
|
@@ -1440,6 +1440,68 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1440
1440
|
addEscalationToTransaction(params, transaction) {
|
|
1441
1441
|
transaction.addCommand(this._escalationInsertSql, this._escalationInsertParams(params), 'void');
|
|
1442
1442
|
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Full-fidelity INSERT for data migration. Preserves the original `id` (UUID),
|
|
1445
|
+
* lifecycle state, and timestamps from the source table. Uses
|
|
1446
|
+
* `ON CONFLICT (id) DO NOTHING` so re-running a migration batch is safe —
|
|
1447
|
+
* rows that already exist are skipped and `null` is returned for them.
|
|
1448
|
+
*/
|
|
1449
|
+
async createEscalationForMigration(params) {
|
|
1450
|
+
const { id, namespace, appId, signalKey, topic, workflowId, taskQueue, workflowType, type, subtype, entity, description, role, priority, originId, parentId, initiatedBy, createdBy, traceId, spanId, escalationPayload, metadata, envelope, expiresAt, status, assignedTo, claimExpiresAt, claimedAt, resolvedAt, resolverPayload, milestones, createdAt, updatedAt, } = params;
|
|
1451
|
+
const result = await this.pgClient.query(`
|
|
1452
|
+
INSERT INTO public.hmsh_escalations
|
|
1453
|
+
(id, namespace, app_id, signal_key, topic, workflow_id, task_queue, workflow_type,
|
|
1454
|
+
type, subtype, entity, description, role, priority,
|
|
1455
|
+
origin_id, parent_id, initiated_by, created_by, trace_id, span_id,
|
|
1456
|
+
escalation_payload, metadata, envelope, expires_at,
|
|
1457
|
+
status, assigned_to, claim_expires_at, claimed_at, resolved_at,
|
|
1458
|
+
resolver_payload, milestones, created_at, updated_at)
|
|
1459
|
+
VALUES
|
|
1460
|
+
($1, $2, $3, $4, $5, $6, $7, $8,
|
|
1461
|
+
$9, $10, $11, $12, $13, $14,
|
|
1462
|
+
$15, $16, $17, $18, $19, $20,
|
|
1463
|
+
$21, $22, $23, $24,
|
|
1464
|
+
$25, $26, $27, $28, $29,
|
|
1465
|
+
$30, $31, $32, $33)
|
|
1466
|
+
ON CONFLICT (id) DO NOTHING
|
|
1467
|
+
RETURNING *
|
|
1468
|
+
`, [
|
|
1469
|
+
id,
|
|
1470
|
+
namespace ?? 'hmsh',
|
|
1471
|
+
appId ?? 'hmsh',
|
|
1472
|
+
signalKey ?? null,
|
|
1473
|
+
topic ?? null,
|
|
1474
|
+
workflowId ?? null,
|
|
1475
|
+
taskQueue ?? null,
|
|
1476
|
+
workflowType ?? null,
|
|
1477
|
+
type ?? null,
|
|
1478
|
+
subtype ?? null,
|
|
1479
|
+
entity ?? null,
|
|
1480
|
+
description ?? null,
|
|
1481
|
+
role ?? null,
|
|
1482
|
+
priority ?? 5,
|
|
1483
|
+
originId ?? null,
|
|
1484
|
+
parentId ?? null,
|
|
1485
|
+
initiatedBy ?? null,
|
|
1486
|
+
createdBy ?? null,
|
|
1487
|
+
traceId ?? null,
|
|
1488
|
+
spanId ?? null,
|
|
1489
|
+
escalationPayload ? JSON.stringify(escalationPayload) : null,
|
|
1490
|
+
metadata ? JSON.stringify(metadata) : null,
|
|
1491
|
+
envelope ? JSON.stringify(envelope) : null,
|
|
1492
|
+
expiresAt ?? null,
|
|
1493
|
+
status ?? 'pending',
|
|
1494
|
+
assignedTo ?? null,
|
|
1495
|
+
claimExpiresAt ?? null,
|
|
1496
|
+
claimedAt ?? null,
|
|
1497
|
+
resolvedAt ?? null,
|
|
1498
|
+
resolverPayload ? JSON.stringify(resolverPayload) : null,
|
|
1499
|
+
milestones ? JSON.stringify(milestones) : '[]',
|
|
1500
|
+
createdAt ?? new Date(),
|
|
1501
|
+
updatedAt ?? new Date(),
|
|
1502
|
+
]);
|
|
1503
|
+
return result.rows[0] ?? null;
|
|
1504
|
+
}
|
|
1443
1505
|
async getEscalation(id, namespace) {
|
|
1444
1506
|
const result = await this.pgClient.query(`SELECT * FROM public.hmsh_escalations WHERE id = $1${namespace ? ' AND namespace = $2' : ''}`, namespace ? [id, namespace] : [id]);
|
|
1445
1507
|
return result.rows[0] ?? null;
|
|
@@ -1449,7 +1511,7 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1449
1511
|
return result.rows[0] ?? null;
|
|
1450
1512
|
}
|
|
1451
1513
|
async listEscalations(params = {}) {
|
|
1452
|
-
const { namespace, role, type, subtype, entity, status, assignedTo, workflowId, originId, limit = 50, offset = 0 } = params;
|
|
1514
|
+
const { namespace, role, roles, type, subtype, entity, status, assignedTo, workflowId, originId, available, sortBy, sortOrder, limit = 50, offset = 0 } = params;
|
|
1453
1515
|
const conditions = [];
|
|
1454
1516
|
const values = [];
|
|
1455
1517
|
let idx = 1;
|
|
@@ -1457,7 +1519,12 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1457
1519
|
conditions.push(`namespace = $${idx++}`);
|
|
1458
1520
|
values.push(namespace);
|
|
1459
1521
|
}
|
|
1460
|
-
|
|
1522
|
+
// roles[] takes precedence over role when both provided
|
|
1523
|
+
if (roles?.length) {
|
|
1524
|
+
conditions.push(`role = ANY($${idx++}::text[])`);
|
|
1525
|
+
values.push(roles);
|
|
1526
|
+
}
|
|
1527
|
+
else if (role) {
|
|
1461
1528
|
conditions.push(`role = $${idx++}`);
|
|
1462
1529
|
values.push(role);
|
|
1463
1530
|
}
|
|
@@ -1489,17 +1556,86 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1489
1556
|
conditions.push(`origin_id = $${idx++}`);
|
|
1490
1557
|
values.push(originId);
|
|
1491
1558
|
}
|
|
1559
|
+
if (available === true) {
|
|
1560
|
+
conditions.push(`(assigned_to IS NULL OR (assigned_until IS NOT NULL AND assigned_until <= NOW()))`);
|
|
1561
|
+
}
|
|
1562
|
+
if (available === false) {
|
|
1563
|
+
conditions.push(`(assigned_to IS NOT NULL AND (assigned_until IS NULL OR assigned_until > NOW()))`);
|
|
1564
|
+
}
|
|
1492
1565
|
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
1566
|
+
const col = sortBy === 'updated_at' ? 'updated_at' : sortBy === 'created_at' ? 'created_at' : 'priority';
|
|
1567
|
+
const dir = sortOrder === 'desc' ? 'DESC' : 'ASC';
|
|
1568
|
+
// Secondary sort is always created_at DESC for deterministic page ordering
|
|
1569
|
+
const orderBy = col === 'priority'
|
|
1570
|
+
? `ORDER BY priority ${dir}, created_at DESC`
|
|
1571
|
+
: `ORDER BY ${col} ${dir}`;
|
|
1493
1572
|
values.push(limit, offset);
|
|
1494
|
-
const result = await this.pgClient.query(`SELECT
|
|
1573
|
+
const result = await this.pgClient.query(`SELECT *,
|
|
1574
|
+
(assigned_to IS NULL OR (assigned_until IS NOT NULL AND assigned_until <= NOW())) AS available
|
|
1575
|
+
FROM public.hmsh_escalations ${where} ${orderBy} LIMIT $${idx++} OFFSET $${idx}`, values);
|
|
1495
1576
|
return result.rows;
|
|
1496
1577
|
}
|
|
1578
|
+
async countEscalations(params = {}) {
|
|
1579
|
+
const { namespace, role, roles, type, subtype, entity, status, assignedTo, workflowId, originId, available } = params;
|
|
1580
|
+
const conditions = [];
|
|
1581
|
+
const values = [];
|
|
1582
|
+
let idx = 1;
|
|
1583
|
+
if (namespace) {
|
|
1584
|
+
conditions.push(`namespace = $${idx++}`);
|
|
1585
|
+
values.push(namespace);
|
|
1586
|
+
}
|
|
1587
|
+
if (roles?.length) {
|
|
1588
|
+
conditions.push(`role = ANY($${idx++}::text[])`);
|
|
1589
|
+
values.push(roles);
|
|
1590
|
+
}
|
|
1591
|
+
else if (role) {
|
|
1592
|
+
conditions.push(`role = $${idx++}`);
|
|
1593
|
+
values.push(role);
|
|
1594
|
+
}
|
|
1595
|
+
if (type) {
|
|
1596
|
+
conditions.push(`type = $${idx++}`);
|
|
1597
|
+
values.push(type);
|
|
1598
|
+
}
|
|
1599
|
+
if (subtype) {
|
|
1600
|
+
conditions.push(`subtype = $${idx++}`);
|
|
1601
|
+
values.push(subtype);
|
|
1602
|
+
}
|
|
1603
|
+
if (entity) {
|
|
1604
|
+
conditions.push(`entity = $${idx++}`);
|
|
1605
|
+
values.push(entity);
|
|
1606
|
+
}
|
|
1607
|
+
if (status) {
|
|
1608
|
+
conditions.push(`status = $${idx++}`);
|
|
1609
|
+
values.push(status);
|
|
1610
|
+
}
|
|
1611
|
+
if (assignedTo) {
|
|
1612
|
+
conditions.push(`assigned_to = $${idx++}`);
|
|
1613
|
+
values.push(assignedTo);
|
|
1614
|
+
}
|
|
1615
|
+
if (workflowId) {
|
|
1616
|
+
conditions.push(`workflow_id = $${idx++}`);
|
|
1617
|
+
values.push(workflowId);
|
|
1618
|
+
}
|
|
1619
|
+
if (originId) {
|
|
1620
|
+
conditions.push(`origin_id = $${idx++}`);
|
|
1621
|
+
values.push(originId);
|
|
1622
|
+
}
|
|
1623
|
+
if (available === true) {
|
|
1624
|
+
conditions.push(`(assigned_to IS NULL OR (assigned_until IS NOT NULL AND assigned_until <= NOW()))`);
|
|
1625
|
+
}
|
|
1626
|
+
if (available === false) {
|
|
1627
|
+
conditions.push(`(assigned_to IS NOT NULL AND (assigned_until IS NULL OR assigned_until > NOW()))`);
|
|
1628
|
+
}
|
|
1629
|
+
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
1630
|
+
const result = await this.pgClient.query(`SELECT COUNT(*)::int AS total FROM public.hmsh_escalations ${where}`, values);
|
|
1631
|
+
return result.rows[0]?.total ?? 0;
|
|
1632
|
+
}
|
|
1497
1633
|
async claimEscalation(params) {
|
|
1498
1634
|
const { id, namespace, assignee = '', durationMinutes = 1 } = params;
|
|
1499
|
-
//
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1635
|
+
// Claims are implicit: status stays 'pending'; availability = assigned_to IS NULL OR assigned_until <= NOW().
|
|
1636
|
+
// all_rows: unlocked read to distinguish 'not-found' vs 'conflict'.
|
|
1637
|
+
// target: locks the row only when it is available (no active claim or same assignee re-claiming).
|
|
1638
|
+
// claimed: writes both assigned_until and claim_expires_at (same value) for service-layer compat.
|
|
1503
1639
|
const result = await this.pgClient.query(`
|
|
1504
1640
|
WITH all_rows AS MATERIALIZED (
|
|
1505
1641
|
SELECT id FROM public.hmsh_escalations
|
|
@@ -1510,16 +1646,16 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1510
1646
|
WHERE id = $1
|
|
1511
1647
|
${namespace ? 'AND namespace = $4' : ''}
|
|
1512
1648
|
AND status = 'pending'
|
|
1513
|
-
AND (assigned_to IS NULL OR
|
|
1649
|
+
AND (assigned_to IS NULL OR assigned_until IS NULL OR assigned_until <= NOW() OR assigned_to = $2)
|
|
1514
1650
|
LIMIT 1
|
|
1515
1651
|
FOR UPDATE SKIP LOCKED
|
|
1516
1652
|
),
|
|
1517
1653
|
claimed AS (
|
|
1518
1654
|
UPDATE public.hmsh_escalations
|
|
1519
|
-
SET
|
|
1520
|
-
assigned_to = $2,
|
|
1655
|
+
SET assigned_to = $2,
|
|
1521
1656
|
claimed_at = NOW(),
|
|
1522
1657
|
claim_expires_at = NOW() + ($3 * INTERVAL '1 minute'),
|
|
1658
|
+
assigned_until = NOW() + ($3 * INTERVAL '1 minute'),
|
|
1523
1659
|
updated_at = NOW()
|
|
1524
1660
|
FROM target WHERE public.hmsh_escalations.id = target.id
|
|
1525
1661
|
RETURNING public.hmsh_escalations.*
|
|
@@ -1538,10 +1674,8 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1538
1674
|
async claimEscalationByMetadata(params) {
|
|
1539
1675
|
const { key, value, namespace, assignee = '', durationMinutes = 1, roles } = params;
|
|
1540
1676
|
const filter = JSON.stringify({ [key]: value });
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
1543
|
-
// claimed: performs the UPDATE atomically from target
|
|
1544
|
-
// The dummy left-join ensures exactly 1 row is always returned so candidatesExist is always visible.
|
|
1677
|
+
// target captures prior_assigned_to to detect re-claim (isExtension).
|
|
1678
|
+
// Implicit claim model: status stays 'pending'; availability = assigned_until IS NULL OR assigned_until <= NOW().
|
|
1545
1679
|
const result = await this.pgClient.query(`
|
|
1546
1680
|
WITH all_candidates AS MATERIALIZED (
|
|
1547
1681
|
SELECT id FROM public.hmsh_escalations
|
|
@@ -1550,25 +1684,25 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1550
1684
|
AND ($4::text[] IS NULL OR role = ANY($4::text[]))
|
|
1551
1685
|
),
|
|
1552
1686
|
target AS MATERIALIZED (
|
|
1553
|
-
SELECT id FROM public.hmsh_escalations
|
|
1687
|
+
SELECT id, assigned_to AS prior_assigned_to FROM public.hmsh_escalations
|
|
1554
1688
|
WHERE ${namespace ? 'namespace = $5 AND' : ''}
|
|
1555
1689
|
metadata @> $1::jsonb
|
|
1556
1690
|
AND ($4::text[] IS NULL OR role = ANY($4::text[]))
|
|
1557
1691
|
AND status = 'pending'
|
|
1558
|
-
AND (assigned_to IS NULL OR
|
|
1692
|
+
AND (assigned_to IS NULL OR assigned_until IS NULL OR assigned_until <= NOW() OR assigned_to = $2)
|
|
1559
1693
|
ORDER BY priority ASC, created_at ASC
|
|
1560
1694
|
LIMIT 1
|
|
1561
1695
|
FOR UPDATE SKIP LOCKED
|
|
1562
1696
|
),
|
|
1563
1697
|
claimed AS (
|
|
1564
1698
|
UPDATE public.hmsh_escalations
|
|
1565
|
-
SET
|
|
1566
|
-
assigned_to = $2,
|
|
1699
|
+
SET assigned_to = $2,
|
|
1567
1700
|
claimed_at = NOW(),
|
|
1568
1701
|
claim_expires_at = NOW() + ($3 * INTERVAL '1 minute'),
|
|
1702
|
+
assigned_until = NOW() + ($3 * INTERVAL '1 minute'),
|
|
1569
1703
|
updated_at = NOW()
|
|
1570
1704
|
FROM target WHERE public.hmsh_escalations.id = target.id
|
|
1571
|
-
RETURNING public.hmsh_escalations
|
|
1705
|
+
RETURNING public.hmsh_escalations.*, target.prior_assigned_to
|
|
1572
1706
|
)
|
|
1573
1707
|
SELECT c.*, (SELECT COUNT(*)::int FROM all_candidates) AS candidates_exist
|
|
1574
1708
|
FROM (VALUES(1)) AS dummy(v)
|
|
@@ -1578,8 +1712,10 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1578
1712
|
: [filter, assignee, durationMinutes, roles ?? null]);
|
|
1579
1713
|
const row = result.rows[0];
|
|
1580
1714
|
const candidatesExist = row?.candidates_exist ?? 0;
|
|
1581
|
-
if (row?.id)
|
|
1582
|
-
|
|
1715
|
+
if (row?.id) {
|
|
1716
|
+
const isExtension = row.prior_assigned_to === assignee;
|
|
1717
|
+
return { ok: true, entry: row, candidatesExist, isExtension };
|
|
1718
|
+
}
|
|
1583
1719
|
return {
|
|
1584
1720
|
ok: false,
|
|
1585
1721
|
reason: candidatesExist > 0 ? 'conflict' : 'not-found',
|
|
@@ -1595,7 +1731,7 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1595
1731
|
WITH target AS MATERIALIZED (
|
|
1596
1732
|
SELECT id, assigned_to FROM public.hmsh_escalations
|
|
1597
1733
|
WHERE id = $1 ${namespace ? 'AND namespace = $3' : ''}
|
|
1598
|
-
AND status = '
|
|
1734
|
+
AND status = 'pending' AND assigned_to IS NOT NULL
|
|
1599
1735
|
FOR UPDATE
|
|
1600
1736
|
),
|
|
1601
1737
|
released AS (
|
|
@@ -1620,6 +1756,7 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1620
1756
|
}
|
|
1621
1757
|
async resolveEscalation(params) {
|
|
1622
1758
|
const { id, namespace, resolverPayload } = params;
|
|
1759
|
+
const payloadJson = resolverPayload ? JSON.stringify(resolverPayload) : null;
|
|
1623
1760
|
const result = await this.pgClient.query(`
|
|
1624
1761
|
WITH target AS MATERIALIZED (
|
|
1625
1762
|
SELECT id, signal_key, topic, status FROM public.hmsh_escalations
|
|
@@ -1634,7 +1771,7 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1634
1771
|
updated_at = NOW()
|
|
1635
1772
|
FROM target
|
|
1636
1773
|
WHERE public.hmsh_escalations.id = target.id
|
|
1637
|
-
AND target.status
|
|
1774
|
+
AND target.status = 'pending'
|
|
1638
1775
|
RETURNING public.hmsh_escalations.id
|
|
1639
1776
|
)
|
|
1640
1777
|
SELECT t.signal_key, t.topic, t.status AS prior_status,
|
|
@@ -1647,8 +1784,8 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1647
1784
|
FROM (SELECT * FROM target) t
|
|
1648
1785
|
FULL OUTER JOIN (SELECT id FROM resolved) r ON r.id = t.id
|
|
1649
1786
|
`, namespace
|
|
1650
|
-
? [id,
|
|
1651
|
-
: [id,
|
|
1787
|
+
? [id, payloadJson, namespace]
|
|
1788
|
+
: [id, payloadJson]);
|
|
1652
1789
|
if (!result.rows[0] || result.rows[0].outcome === 'not-found')
|
|
1653
1790
|
return { ok: false, reason: 'not-found' };
|
|
1654
1791
|
if (result.rows[0].outcome === 'already-cancelled')
|
|
@@ -1660,6 +1797,7 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1660
1797
|
async resolveEscalationByMetadata(params) {
|
|
1661
1798
|
const { key, value, namespace, resolverPayload, roles } = params;
|
|
1662
1799
|
const filter = JSON.stringify({ [key]: value });
|
|
1800
|
+
const payloadJson = resolverPayload ? JSON.stringify(resolverPayload) : null;
|
|
1663
1801
|
const result = await this.pgClient.query(`
|
|
1664
1802
|
WITH target AS MATERIALIZED (
|
|
1665
1803
|
SELECT id, signal_key, topic, status FROM public.hmsh_escalations
|
|
@@ -1677,7 +1815,7 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1677
1815
|
updated_at = NOW()
|
|
1678
1816
|
FROM target
|
|
1679
1817
|
WHERE public.hmsh_escalations.id = target.id
|
|
1680
|
-
AND target.status
|
|
1818
|
+
AND target.status = 'pending'
|
|
1681
1819
|
RETURNING public.hmsh_escalations.id
|
|
1682
1820
|
)
|
|
1683
1821
|
SELECT t.signal_key, t.topic, t.status AS prior_status,
|
|
@@ -1690,8 +1828,8 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1690
1828
|
FROM (SELECT * FROM target) t
|
|
1691
1829
|
FULL OUTER JOIN (SELECT id FROM resolved) r ON r.id = t.id
|
|
1692
1830
|
`, namespace
|
|
1693
|
-
? [filter,
|
|
1694
|
-
: [filter,
|
|
1831
|
+
? [filter, payloadJson, roles ?? null, namespace]
|
|
1832
|
+
: [filter, payloadJson, roles ?? null]);
|
|
1695
1833
|
if (!result.rows[0] || result.rows[0].outcome === 'not-found')
|
|
1696
1834
|
return { ok: false, reason: 'not-found' };
|
|
1697
1835
|
if (result.rows[0].outcome === 'already-cancelled')
|
|
@@ -1747,7 +1885,7 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1747
1885
|
SET status = 'cancelled', updated_at = NOW()
|
|
1748
1886
|
FROM target
|
|
1749
1887
|
WHERE public.hmsh_escalations.id = target.id
|
|
1750
|
-
AND target.status
|
|
1888
|
+
AND target.status = 'pending'
|
|
1751
1889
|
RETURNING public.hmsh_escalations.id
|
|
1752
1890
|
)
|
|
1753
1891
|
SELECT t.id, t.status AS prior_status,
|
|
@@ -1777,7 +1915,7 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1777
1915
|
claim_expires_at = NULL,
|
|
1778
1916
|
updated_at = NOW()
|
|
1779
1917
|
WHERE id = $1 ${namespace ? 'AND namespace = $3' : ''}
|
|
1780
|
-
AND status
|
|
1918
|
+
AND status = 'pending'
|
|
1781
1919
|
RETURNING *
|
|
1782
1920
|
`, namespace ? [id, targetRole, namespace] : [id, targetRole]);
|
|
1783
1921
|
return result.rows[0] ?? null;
|
|
@@ -1853,16 +1991,10 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1853
1991
|
`, namespace ? [id, JSON.stringify(stamped), namespace] : [id, JSON.stringify(stamped)]);
|
|
1854
1992
|
return result.rows[0] ?? null;
|
|
1855
1993
|
}
|
|
1856
|
-
async releaseExpiredEscalations(
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
claimed_at = NULL, claim_expires_at = NULL, updated_at = NOW()
|
|
1861
|
-
WHERE status = 'claimed'
|
|
1862
|
-
AND claim_expires_at <= NOW()
|
|
1863
|
-
${namespace ? 'AND namespace = $1' : ''}
|
|
1864
|
-
`, namespace ? [namespace] : []);
|
|
1865
|
-
return result.rowCount ?? 0;
|
|
1994
|
+
async releaseExpiredEscalations(_namespace) {
|
|
1995
|
+
// No-op in the implicit claim model: availability is computed at query time from
|
|
1996
|
+
// assigned_until, so no background sweep is needed to release expired claims.
|
|
1997
|
+
return 0;
|
|
1866
1998
|
}
|
|
1867
1999
|
}
|
|
1868
2000
|
exports.PostgresStoreService = PostgresStoreService;
|
|
@@ -34,7 +34,8 @@ export interface EscalationEntry {
|
|
|
34
34
|
entity: string | null;
|
|
35
35
|
description: string | null;
|
|
36
36
|
role: string | null;
|
|
37
|
-
status: 'pending'
|
|
37
|
+
/** Lifecycle status. Claims are implicit: status='pending' + assigned_to IS NOT NULL + assigned_until > NOW(). */
|
|
38
|
+
status: 'pending' | 'resolved' | 'cancelled' | 'expired';
|
|
38
39
|
priority: number;
|
|
39
40
|
assigned_to: string | null;
|
|
40
41
|
assigned_until: Date | null;
|
|
@@ -55,6 +56,8 @@ export interface EscalationEntry {
|
|
|
55
56
|
expires_at: Date | null;
|
|
56
57
|
created_at: Date;
|
|
57
58
|
updated_at: Date;
|
|
59
|
+
/** Computed by list(): true when the row is claimable (no active assignee or expired claim). */
|
|
60
|
+
available?: boolean;
|
|
58
61
|
}
|
|
59
62
|
/**
|
|
60
63
|
* Result of `claim()` — identifies whether failure was due to the row not
|
|
@@ -70,15 +73,15 @@ export type ClaimEscalationResult = {
|
|
|
70
73
|
reason: 'not-found' | 'conflict';
|
|
71
74
|
};
|
|
72
75
|
/**
|
|
73
|
-
* Result of `claimByMetadata()`. Includes `candidatesExist`
|
|
74
|
-
* count of rows matching the
|
|
75
|
-
*
|
|
76
|
-
* all are locked or in-progress".
|
|
76
|
+
* Result of `claimByMetadata()`. Includes `candidatesExist` and `isExtension`:
|
|
77
|
+
* - `candidatesExist` — total count of rows matching the filter regardless of claimability
|
|
78
|
+
* - `isExtension` — true when the same assignee re-claims a row they already hold (extends the expiry)
|
|
77
79
|
*/
|
|
78
80
|
export type ClaimByMetadataResult = {
|
|
79
81
|
ok: true;
|
|
80
82
|
entry: EscalationEntry;
|
|
81
83
|
candidatesExist: number;
|
|
84
|
+
isExtension: boolean;
|
|
82
85
|
} | {
|
|
83
86
|
ok: false;
|
|
84
87
|
reason: 'not-found' | 'conflict';
|
|
@@ -88,7 +91,7 @@ export type ResolveEscalationResult = {
|
|
|
88
91
|
ok: true;
|
|
89
92
|
} | {
|
|
90
93
|
ok: false;
|
|
91
|
-
reason: 'not-found' | 'already-resolved' | 'already-cancelled'
|
|
94
|
+
reason: 'not-found' | 'already-resolved' | 'already-cancelled';
|
|
92
95
|
};
|
|
93
96
|
export type ReleaseEscalationResult = {
|
|
94
97
|
ok: true;
|
|
@@ -105,6 +108,8 @@ export type CancelEscalationResult = {
|
|
|
105
108
|
export interface ListEscalationsParams {
|
|
106
109
|
namespace?: string;
|
|
107
110
|
role?: string;
|
|
111
|
+
/** Filter by one or more roles (OR semantics; takes precedence over `role` when both set). */
|
|
112
|
+
roles?: string[];
|
|
108
113
|
type?: string;
|
|
109
114
|
subtype?: string;
|
|
110
115
|
entity?: string;
|
|
@@ -112,6 +117,10 @@ export interface ListEscalationsParams {
|
|
|
112
117
|
assignedTo?: string;
|
|
113
118
|
workflowId?: string;
|
|
114
119
|
originId?: string;
|
|
120
|
+
/** When true, returns only rows without an active claim. When false, returns only actively claimed rows. */
|
|
121
|
+
available?: boolean;
|
|
122
|
+
sortBy?: 'created_at' | 'priority' | 'updated_at';
|
|
123
|
+
sortOrder?: 'asc' | 'desc';
|
|
115
124
|
limit?: number;
|
|
116
125
|
offset?: number;
|
|
117
126
|
}
|
|
@@ -210,3 +219,31 @@ export interface EscalateToRoleParams {
|
|
|
210
219
|
targetRole: string;
|
|
211
220
|
namespace?: string;
|
|
212
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Full-fidelity migration params. Extends `CreateEscalationParams` with:
|
|
224
|
+
* - `id` (required) — preserves the original UUID; no auto-generation
|
|
225
|
+
* - lifecycle state fields (`status`, `assignedTo`, `claimExpiresAt`, …) — carry over
|
|
226
|
+
* the exact state of the migrated row so in-flight escalations land correctly
|
|
227
|
+
* - `createdAt` / `updatedAt` — preserve original timestamps
|
|
228
|
+
*
|
|
229
|
+
* The underlying INSERT uses `ON CONFLICT (id) DO NOTHING`, so calling
|
|
230
|
+
* `migrate()` multiple times with the same ID is safe — subsequent calls
|
|
231
|
+
* return `null` without touching the existing row.
|
|
232
|
+
*/
|
|
233
|
+
export interface MigrateEscalationParams extends CreateEscalationParams {
|
|
234
|
+
/** Required — preserve the original UUID from the source table. */
|
|
235
|
+
id: string;
|
|
236
|
+
status?: 'pending' | 'claimed' | 'resolved' | 'cancelled' | 'expired';
|
|
237
|
+
assignedTo?: string;
|
|
238
|
+
claimExpiresAt?: Date;
|
|
239
|
+
claimedAt?: Date;
|
|
240
|
+
resolvedAt?: Date;
|
|
241
|
+
resolverPayload?: Record<string, unknown>;
|
|
242
|
+
milestones?: Array<{
|
|
243
|
+
name: string;
|
|
244
|
+
value: unknown;
|
|
245
|
+
[key: string]: unknown;
|
|
246
|
+
}>;
|
|
247
|
+
createdAt?: Date;
|
|
248
|
+
updatedAt?: Date;
|
|
249
|
+
}
|
package/build/types/index.d.ts
CHANGED
|
@@ -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 { ConditionQueueConfig, EscalationEntry, ClaimEscalationResult, ClaimByMetadataResult, ReleaseEscalationResult, ResolveEscalationResult, CancelEscalationResult, ListEscalationsParams, CreateEscalationParams, UpdateEscalationParams, AppendMilestonesParams, ClaimEscalationParams, ClaimByMetadataParams, ReleaseEscalationParams, ResolveEscalationParams, ResolveByMetadataParams, EscalateToRoleParams, } from './hmsh_escalations';
|
|
28
|
+
export { ConditionQueueConfig, EscalationEntry, ClaimEscalationResult, ClaimByMetadataResult, ReleaseEscalationResult, ResolveEscalationResult, CancelEscalationResult, ListEscalationsParams, CreateEscalationParams, UpdateEscalationParams, AppendMilestonesParams, ClaimEscalationParams, ClaimByMetadataParams, ReleaseEscalationParams, ResolveEscalationParams, ResolveByMetadataParams, EscalateToRoleParams, MigrateEscalationParams, } from './hmsh_escalations';
|
package/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import * as KeyStore from './modules/key';
|
|
|
18
18
|
import { ConnectorService as Connector } from './services/connector/factory';
|
|
19
19
|
import { PostgresConnection as ConnectorPostgres } from './services/connector/providers/postgres';
|
|
20
20
|
import { NatsConnection as ConnectorNATS } from './services/connector/providers/nats';
|
|
21
|
+
import { Escalations } from './services/escalations';
|
|
21
22
|
|
|
22
23
|
export {
|
|
23
24
|
//Provider Connectors
|
|
@@ -30,6 +31,7 @@ export {
|
|
|
30
31
|
HotMeshConfig,
|
|
31
32
|
Virtual,
|
|
32
33
|
Durable,
|
|
34
|
+
Escalations,
|
|
33
35
|
DBA,
|
|
34
36
|
|
|
35
37
|
//Durable Submodules
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.2",
|
|
4
4
|
"description": "Durable Workflow",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
@@ -50,7 +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:
|
|
53
|
+
"test:durable:escalations": "HMSH_LOGLEVEL=info vitest run tests/durable/escalations",
|
|
54
54
|
"test:durable:exporter": "HMSH_LOGLEVEL=info vitest run tests/durable/exporter",
|
|
55
55
|
"test:durable:exporter:debug": "EXPORT_DEBUG=1 HMSH_LOGLEVEL=error vitest run tests/durable/basic/postgres.test.ts",
|
|
56
56
|
"test:durable:codec": "vitest run tests/durable/codec/postgres.test.ts",
|
|
@@ -85,7 +85,13 @@
|
|
|
85
85
|
"test:sub:nats": "vitest run tests/functional/sub/providers/nats/nats.test.ts",
|
|
86
86
|
"test:trigger": "vitest run tests/unit/services/activities/trigger.test.ts",
|
|
87
87
|
"test:virtual": "vitest run tests/virtual",
|
|
88
|
-
"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"
|
|
89
95
|
},
|
|
90
96
|
"keywords": [
|
|
91
97
|
"Invisible Infrastructure",
|