@hotmeshio/hotmesh 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/package.json +1 -1
- package/build/services/durable/worker.js +6 -1
- package/build/services/escalations/client.d.ts +14 -0
- package/build/services/escalations/client.js +16 -2
- package/build/services/store/providers/postgres/kvtables.js +37 -0
- package/build/services/store/providers/postgres/postgres.js +76 -40
- package/build/types/hmsh_escalations.d.ts +19 -0
- package/package.json +1 -1
package/build/package.json
CHANGED
|
@@ -885,7 +885,12 @@ class WorkerService {
|
|
|
885
885
|
code: enums_1.HMSH_CODE_DURABLE_ALL,
|
|
886
886
|
items: [...interruptionRegistry],
|
|
887
887
|
size: interruptionRegistry.length,
|
|
888
|
-
|
|
888
|
+
// Use the items' execution dimension (all items in one Promise.all share it),
|
|
889
|
+
// not the raw input dimension. In a continued generation (post-continueAsNew)
|
|
890
|
+
// the effective dimension is the generation prefix `$N`, which the items carry
|
|
891
|
+
// but `workflowInput.workflowDimension` does not — mirrors the single-condition
|
|
892
|
+
// path above so the collator's harvested markers land where condition() replays.
|
|
893
|
+
workflowDimension: interruptionRegistry[0].workflowDimension || workflowDimension || '',
|
|
889
894
|
index: execIndex,
|
|
890
895
|
originJobId: originJobId || workflowId,
|
|
891
896
|
parentWorkflowId: workflowId,
|
|
@@ -143,11 +143,21 @@ export declare class EscalationClientService {
|
|
|
143
143
|
* durable proof. Signal delivery is best-effort post-commit — the resolved
|
|
144
144
|
* row is the recovery record for any missed delivery. Returns the updated
|
|
145
145
|
* row as `entry` on success.
|
|
146
|
+
*
|
|
147
|
+
* Pass `params.metadata` to merge the resolution outcome ("what actually
|
|
148
|
+
* happened") into the row's GIN-indexed `metadata` in the same atomic UPDATE,
|
|
149
|
+
* making it `@>`-queryable alongside the creation metadata ("what was
|
|
150
|
+
* intended"). This is distinct from `resolverPayload`, which is delivered to
|
|
151
|
+
* the waiting workflow as `condition()`'s return value and is not GIN-indexed.
|
|
146
152
|
*/
|
|
147
153
|
resolve(params: ResolveEscalationParams, namespace?: string): Promise<ResolveEscalationResult>;
|
|
148
154
|
/**
|
|
149
155
|
* Resolves the highest-priority matching escalation by metadata filter,
|
|
150
156
|
* then delivers its signal. Same transaction + WHERE guard semantics as `resolve()`.
|
|
157
|
+
*
|
|
158
|
+
* The `key`/`value` are the *selector* used to find the row; pass
|
|
159
|
+
* `params.metadata` to additionally merge a resolution patch into that row's
|
|
160
|
+
* GIN-indexed `metadata` in the same atomic UPDATE. See {@link resolve}.
|
|
151
161
|
*/
|
|
152
162
|
resolveByMetadata(params: ResolveByMetadataParams, namespace?: string): Promise<ResolveEscalationResult>;
|
|
153
163
|
/**
|
|
@@ -182,6 +192,10 @@ export declare class EscalationClientService {
|
|
|
182
192
|
* Bulk-resolves pending escalations by id-set. No signal delivery — intended
|
|
183
193
|
* for redirect-to-triage flows where no workflow is waiting. Returns the
|
|
184
194
|
* resolved rows.
|
|
195
|
+
*
|
|
196
|
+
* Pass `params.metadata` to merge a resolution patch into every winning
|
|
197
|
+
* (still-pending) row's GIN-indexed `metadata` in the single atomic UPDATE.
|
|
198
|
+
* See {@link resolve}.
|
|
185
199
|
*/
|
|
186
200
|
resolveMany(params: ResolveManyParams): Promise<EscalationEntry[]>;
|
|
187
201
|
/**
|
|
@@ -281,12 +281,18 @@ class EscalationClientService {
|
|
|
281
281
|
* durable proof. Signal delivery is best-effort post-commit — the resolved
|
|
282
282
|
* row is the recovery record for any missed delivery. Returns the updated
|
|
283
283
|
* row as `entry` on success.
|
|
284
|
+
*
|
|
285
|
+
* Pass `params.metadata` to merge the resolution outcome ("what actually
|
|
286
|
+
* happened") into the row's GIN-indexed `metadata` in the same atomic UPDATE,
|
|
287
|
+
* making it `@>`-queryable alongside the creation metadata ("what was
|
|
288
|
+
* intended"). This is distinct from `resolverPayload`, which is delivered to
|
|
289
|
+
* the waiting workflow as `condition()`'s return value and is not GIN-indexed.
|
|
284
290
|
*/
|
|
285
291
|
async resolve(params, namespace) {
|
|
286
292
|
const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
|
|
287
293
|
const hm = await this._engine(null, ns);
|
|
288
294
|
const store = hm.engine.store;
|
|
289
|
-
const dbResult = await store.resolveEscalation({ id: params.id, resolverPayload: params.resolverPayload });
|
|
295
|
+
const dbResult = await store.resolveEscalation({ id: params.id, resolverPayload: params.resolverPayload, metadata: params.metadata });
|
|
290
296
|
if (!dbResult.ok)
|
|
291
297
|
return dbResult;
|
|
292
298
|
if (dbResult.signalKey) {
|
|
@@ -301,12 +307,16 @@ class EscalationClientService {
|
|
|
301
307
|
/**
|
|
302
308
|
* Resolves the highest-priority matching escalation by metadata filter,
|
|
303
309
|
* then delivers its signal. Same transaction + WHERE guard semantics as `resolve()`.
|
|
310
|
+
*
|
|
311
|
+
* The `key`/`value` are the *selector* used to find the row; pass
|
|
312
|
+
* `params.metadata` to additionally merge a resolution patch into that row's
|
|
313
|
+
* GIN-indexed `metadata` in the same atomic UPDATE. See {@link resolve}.
|
|
304
314
|
*/
|
|
305
315
|
async resolveByMetadata(params, namespace) {
|
|
306
316
|
const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
|
|
307
317
|
const hm = await this._engine(null, ns);
|
|
308
318
|
const store = hm.engine.store;
|
|
309
|
-
const dbResult = await store.resolveEscalationByMetadata({ key: params.key, value: params.value, resolverPayload: params.resolverPayload, roles: params.roles });
|
|
319
|
+
const dbResult = await store.resolveEscalationByMetadata({ key: params.key, value: params.value, resolverPayload: params.resolverPayload, roles: params.roles, metadata: params.metadata });
|
|
310
320
|
if (!dbResult.ok)
|
|
311
321
|
return dbResult;
|
|
312
322
|
if (dbResult.signalKey) {
|
|
@@ -366,6 +376,10 @@ class EscalationClientService {
|
|
|
366
376
|
* Bulk-resolves pending escalations by id-set. No signal delivery — intended
|
|
367
377
|
* for redirect-to-triage flows where no workflow is waiting. Returns the
|
|
368
378
|
* resolved rows.
|
|
379
|
+
*
|
|
380
|
+
* Pass `params.metadata` to merge a resolution patch into every winning
|
|
381
|
+
* (still-pending) row's GIN-indexed `metadata` in the single atomic UPDATE.
|
|
382
|
+
* See {@link resolve}.
|
|
369
383
|
*/
|
|
370
384
|
async resolveMany(params) {
|
|
371
385
|
const hm = await this._engine(null, params.namespace);
|
|
@@ -315,6 +315,26 @@ const KVTables = (context) => ({
|
|
|
315
315
|
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_task
|
|
316
316
|
ON public.hmsh_escalations(task_id)
|
|
317
317
|
WHERE task_id IS NOT NULL;
|
|
318
|
+
`);
|
|
319
|
+
// v0.25.0: escalationStats indexes. Callers filter by role (optional) and
|
|
320
|
+
// namespace (optional) — never namespace-first — so the pending index leads
|
|
321
|
+
// with role and the two windowed indexes lead with their time column: the
|
|
322
|
+
// scan stays window-bounded even for unfiltered (admin) calls. Trailing
|
|
323
|
+
// columns cover every column the stats aggregates touch, keeping each of
|
|
324
|
+
// the three CTE scans index-only.
|
|
325
|
+
await client.query(`
|
|
326
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_stats_pending
|
|
327
|
+
ON public.hmsh_escalations(role, type, assigned_to, assigned_until, namespace)
|
|
328
|
+
WHERE status = 'pending';
|
|
329
|
+
`);
|
|
330
|
+
await client.query(`
|
|
331
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_stats_created
|
|
332
|
+
ON public.hmsh_escalations(created_at, role, namespace);
|
|
333
|
+
`);
|
|
334
|
+
await client.query(`
|
|
335
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_stats_resolved
|
|
336
|
+
ON public.hmsh_escalations(resolved_at, type, role, namespace)
|
|
337
|
+
WHERE status = 'resolved';
|
|
318
338
|
`);
|
|
319
339
|
await client.query('COMMIT');
|
|
320
340
|
},
|
|
@@ -440,6 +460,23 @@ const KVTables = (context) => ({
|
|
|
440
460
|
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_task
|
|
441
461
|
ON public.hmsh_escalations(task_id)
|
|
442
462
|
WHERE task_id IS NOT NULL;
|
|
463
|
+
`);
|
|
464
|
+
// escalationStats indexes: role/time-led so aggregate scans stay
|
|
465
|
+
// bounded (backlog or window) and index-only. See migrate() for
|
|
466
|
+
// the column-order rationale.
|
|
467
|
+
await client.query(`
|
|
468
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_stats_pending
|
|
469
|
+
ON public.hmsh_escalations(role, type, assigned_to, assigned_until, namespace)
|
|
470
|
+
WHERE status = 'pending';
|
|
471
|
+
`);
|
|
472
|
+
await client.query(`
|
|
473
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_stats_created
|
|
474
|
+
ON public.hmsh_escalations(created_at, role, namespace);
|
|
475
|
+
`);
|
|
476
|
+
await client.query(`
|
|
477
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_stats_resolved
|
|
478
|
+
ON public.hmsh_escalations(resolved_at, type, role, namespace)
|
|
479
|
+
WHERE status = 'resolved';
|
|
443
480
|
`);
|
|
444
481
|
break;
|
|
445
482
|
case 'relational_connection':
|
|
@@ -1769,8 +1769,11 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1769
1769
|
return { ok: true, entry: row.entry_json };
|
|
1770
1770
|
}
|
|
1771
1771
|
async resolveEscalation(params) {
|
|
1772
|
-
const { id, namespace, resolverPayload } = params;
|
|
1772
|
+
const { id, namespace, resolverPayload, metadata } = params;
|
|
1773
1773
|
const payloadJson = resolverPayload ? JSON.stringify(resolverPayload) : null;
|
|
1774
|
+
// metaJson is bound at a fixed index; the CASE no-ops when null so resolution
|
|
1775
|
+
// can merge into the GIN-indexed metadata without disturbing the param layout.
|
|
1776
|
+
const metaJson = metadata ? JSON.stringify(metadata) : null;
|
|
1774
1777
|
// Explicit transaction: FOR UPDATE locks the row; WHERE guard on UPDATE is the TOCTOU
|
|
1775
1778
|
// barrier — a concurrent caller whose UPDATE matches 0 rows sees 'already-resolved'.
|
|
1776
1779
|
// On crash before COMMIT neither write lands; on crash after COMMIT both are durable.
|
|
@@ -1789,9 +1792,13 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1789
1792
|
return { ok: false, reason: 'already-cancelled' };
|
|
1790
1793
|
}
|
|
1791
1794
|
const updateResult = await this.pgClient.query(`UPDATE public.hmsh_escalations
|
|
1792
|
-
SET status = 'resolved', resolved_at = NOW(), resolver_payload = $2,
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
+
SET status = 'resolved', resolved_at = NOW(), resolver_payload = $2,
|
|
1796
|
+
metadata = CASE WHEN $3::jsonb IS NOT NULL
|
|
1797
|
+
THEN COALESCE(metadata, '{}'::jsonb) || $3::jsonb
|
|
1798
|
+
ELSE metadata END,
|
|
1799
|
+
updated_at = NOW()
|
|
1800
|
+
WHERE id = $1 ${namespace ? 'AND namespace = $4' : ''} AND status = 'pending'
|
|
1801
|
+
RETURNING *`, namespace ? [id, payloadJson, metaJson, namespace] : [id, payloadJson, metaJson]);
|
|
1795
1802
|
if (!updateResult.rows[0]) {
|
|
1796
1803
|
await this.pgClient.query('ROLLBACK');
|
|
1797
1804
|
return { ok: false, reason: 'already-resolved' };
|
|
@@ -1805,9 +1812,10 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1805
1812
|
}
|
|
1806
1813
|
}
|
|
1807
1814
|
async resolveEscalationByMetadata(params) {
|
|
1808
|
-
const { key, value, namespace, resolverPayload, roles } = params;
|
|
1815
|
+
const { key, value, namespace, resolverPayload, roles, metadata } = params;
|
|
1809
1816
|
const filter = JSON.stringify({ [key]: value });
|
|
1810
1817
|
const payloadJson = resolverPayload ? JSON.stringify(resolverPayload) : null;
|
|
1818
|
+
const metaJson = metadata ? JSON.stringify(metadata) : null;
|
|
1811
1819
|
await this.pgClient.query('BEGIN');
|
|
1812
1820
|
try {
|
|
1813
1821
|
const lockResult = await this.pgClient.query(`SELECT id, signal_key, topic, status FROM public.hmsh_escalations
|
|
@@ -1827,9 +1835,13 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
1827
1835
|
return { ok: false, reason: 'already-cancelled' };
|
|
1828
1836
|
}
|
|
1829
1837
|
const updateResult = await this.pgClient.query(`UPDATE public.hmsh_escalations
|
|
1830
|
-
SET status = 'resolved', resolved_at = NOW(), resolver_payload = $2,
|
|
1838
|
+
SET status = 'resolved', resolved_at = NOW(), resolver_payload = $2,
|
|
1839
|
+
metadata = CASE WHEN $3::jsonb IS NOT NULL
|
|
1840
|
+
THEN COALESCE(metadata, '{}'::jsonb) || $3::jsonb
|
|
1841
|
+
ELSE metadata END,
|
|
1842
|
+
updated_at = NOW()
|
|
1831
1843
|
WHERE id = $1 AND status = 'pending'
|
|
1832
|
-
RETURNING *`, [id, payloadJson]);
|
|
1844
|
+
RETURNING *`, [id, payloadJson, metaJson]);
|
|
1833
1845
|
if (!updateResult.rows[0]) {
|
|
1834
1846
|
await this.pgClient.query('ROLLBACK');
|
|
1835
1847
|
return { ok: false, reason: 'already-resolved' };
|
|
@@ -2007,17 +2019,21 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
2007
2019
|
return result.rowCount ?? 0;
|
|
2008
2020
|
}
|
|
2009
2021
|
async resolveManyEscalations(params) {
|
|
2010
|
-
const { ids, namespace, resolverPayload } = params;
|
|
2022
|
+
const { ids, namespace, resolverPayload, metadata } = params;
|
|
2011
2023
|
const payloadJson = resolverPayload ? JSON.stringify(resolverPayload) : null;
|
|
2024
|
+
const metaJson = metadata ? JSON.stringify(metadata) : null;
|
|
2012
2025
|
const result = await this.pgClient.query(`UPDATE public.hmsh_escalations
|
|
2013
2026
|
SET status = 'resolved',
|
|
2014
2027
|
resolved_at = NOW(),
|
|
2015
2028
|
resolver_payload = $1,
|
|
2029
|
+
metadata = CASE WHEN $3::jsonb IS NOT NULL
|
|
2030
|
+
THEN COALESCE(metadata, '{}'::jsonb) || $3::jsonb
|
|
2031
|
+
ELSE metadata END,
|
|
2016
2032
|
updated_at = NOW()
|
|
2017
2033
|
WHERE id = ANY($2::uuid[])
|
|
2018
|
-
${namespace ? 'AND namespace = $
|
|
2034
|
+
${namespace ? 'AND namespace = $4' : ''}
|
|
2019
2035
|
AND status = 'pending'
|
|
2020
|
-
RETURNING *`, namespace ? [payloadJson, ids, namespace] : [payloadJson, ids]);
|
|
2036
|
+
RETURNING *`, namespace ? [payloadJson, ids, metaJson, namespace] : [payloadJson, ids, metaJson]);
|
|
2021
2037
|
return result.rows;
|
|
2022
2038
|
}
|
|
2023
2039
|
async escalationStats(params = {}) {
|
|
@@ -2035,45 +2051,65 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
2035
2051
|
queryParams.push(namespace);
|
|
2036
2052
|
if (roles?.length)
|
|
2037
2053
|
queryParams.push(roles);
|
|
2054
|
+
// Three bounded sources instead of one all-history scan (v0.25.0):
|
|
2055
|
+
// backlog — status='pending' partial (bounded by queue depth, never history)
|
|
2056
|
+
// created — created_at window range (all statuses)
|
|
2057
|
+
// resolved — status='resolved' + resolved_at window range
|
|
2058
|
+
// Totals derive from the grouped CTEs (NULL role/type groups included, so
|
|
2059
|
+
// totals match the pre-0.25.0 all-row counts); NULL groups are excluded
|
|
2060
|
+
// only from the by_role/by_type rollups, as before. resolved_at is only
|
|
2061
|
+
// ever set alongside status='resolved' (and never survives a transition
|
|
2062
|
+
// back to pending), so the status predicate is a pure index enabler.
|
|
2063
|
+
// Roles/types with no pending rows and no window activity no longer emit
|
|
2064
|
+
// zero-count rollup entries.
|
|
2038
2065
|
const result = await this.pgClient.query(`
|
|
2039
|
-
WITH
|
|
2040
|
-
SELECT * FROM public.hmsh_escalations
|
|
2041
|
-
WHERE true ${nsClause} ${rolClause}
|
|
2042
|
-
),
|
|
2043
|
-
totals AS (
|
|
2044
|
-
SELECT
|
|
2045
|
-
COUNT(*) FILTER (WHERE status = 'pending')::int AS pending,
|
|
2046
|
-
COUNT(*) FILTER (WHERE status = 'pending'
|
|
2047
|
-
AND assigned_to IS NOT NULL
|
|
2048
|
-
AND assigned_until IS NOT NULL AND assigned_until > NOW())::int AS claimed,
|
|
2049
|
-
COUNT(*) FILTER (WHERE created_at >= NOW() - ($1 * INTERVAL '1 hour'))::int AS created,
|
|
2050
|
-
COUNT(*) FILTER (WHERE resolved_at >= NOW() - ($1 * INTERVAL '1 hour'))::int AS resolved
|
|
2051
|
-
FROM base
|
|
2052
|
-
),
|
|
2053
|
-
by_role AS (
|
|
2066
|
+
WITH pending_by_role AS (
|
|
2054
2067
|
SELECT role,
|
|
2055
|
-
COUNT(*)
|
|
2056
|
-
COUNT(*) FILTER (WHERE
|
|
2057
|
-
AND assigned_to IS NOT NULL
|
|
2068
|
+
COUNT(*)::int AS pending,
|
|
2069
|
+
COUNT(*) FILTER (WHERE assigned_to IS NOT NULL
|
|
2058
2070
|
AND assigned_until IS NOT NULL AND assigned_until > NOW())::int AS claimed
|
|
2059
|
-
FROM
|
|
2071
|
+
FROM public.hmsh_escalations
|
|
2072
|
+
WHERE status = 'pending' ${nsClause} ${rolClause}
|
|
2060
2073
|
GROUP BY role
|
|
2061
2074
|
),
|
|
2062
|
-
|
|
2075
|
+
pending_by_type AS (
|
|
2063
2076
|
SELECT type,
|
|
2064
|
-
COUNT(*)
|
|
2065
|
-
COUNT(*) FILTER (WHERE
|
|
2066
|
-
AND
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2077
|
+
COUNT(*)::int AS pending,
|
|
2078
|
+
COUNT(*) FILTER (WHERE assigned_to IS NOT NULL
|
|
2079
|
+
AND assigned_until IS NOT NULL AND assigned_until > NOW())::int AS claimed
|
|
2080
|
+
FROM public.hmsh_escalations
|
|
2081
|
+
WHERE status = 'pending' ${nsClause} ${rolClause}
|
|
2082
|
+
GROUP BY type
|
|
2083
|
+
),
|
|
2084
|
+
created_agg AS (
|
|
2085
|
+
SELECT COUNT(*)::int AS created
|
|
2086
|
+
FROM public.hmsh_escalations
|
|
2087
|
+
WHERE created_at >= NOW() - ($1 * INTERVAL '1 hour') ${nsClause} ${rolClause}
|
|
2088
|
+
),
|
|
2089
|
+
resolved_by_type AS (
|
|
2090
|
+
SELECT type, COUNT(*)::int AS resolved
|
|
2091
|
+
FROM public.hmsh_escalations
|
|
2092
|
+
WHERE status = 'resolved'
|
|
2093
|
+
AND resolved_at >= NOW() - ($1 * INTERVAL '1 hour') ${nsClause} ${rolClause}
|
|
2070
2094
|
GROUP BY type
|
|
2071
2095
|
)
|
|
2072
2096
|
SELECT
|
|
2073
|
-
|
|
2074
|
-
COALESCE((SELECT
|
|
2075
|
-
|
|
2076
|
-
|
|
2097
|
+
COALESCE((SELECT SUM(pending)::int FROM pending_by_role), 0) AS pending,
|
|
2098
|
+
COALESCE((SELECT SUM(claimed)::int FROM pending_by_role), 0) AS claimed,
|
|
2099
|
+
(SELECT created FROM created_agg) AS created,
|
|
2100
|
+
COALESCE((SELECT SUM(resolved)::int FROM resolved_by_type), 0) AS resolved,
|
|
2101
|
+
COALESCE((SELECT json_agg(json_build_object('role',role,'pending',pending,'claimed',claimed))
|
|
2102
|
+
FROM pending_by_role WHERE role IS NOT NULL), '[]'::json) AS by_role,
|
|
2103
|
+
COALESCE((SELECT json_agg(json_build_object('type',t.type,'pending',t.pending,'claimed',t.claimed,'resolved',t.resolved))
|
|
2104
|
+
FROM (
|
|
2105
|
+
SELECT COALESCE(p.type, r.type) AS type,
|
|
2106
|
+
COALESCE(p.pending, 0) AS pending,
|
|
2107
|
+
COALESCE(p.claimed, 0) AS claimed,
|
|
2108
|
+
COALESCE(r.resolved, 0) AS resolved
|
|
2109
|
+
FROM pending_by_type p
|
|
2110
|
+
FULL OUTER JOIN resolved_by_type r ON r.type = p.type
|
|
2111
|
+
WHERE COALESCE(p.type, r.type) IS NOT NULL
|
|
2112
|
+
) t), '[]'::json) AS by_type
|
|
2077
2113
|
`, queryParams);
|
|
2078
2114
|
const row = result.rows[0];
|
|
2079
2115
|
return {
|
|
@@ -256,6 +256,14 @@ export interface ResolveEscalationParams {
|
|
|
256
256
|
id: string;
|
|
257
257
|
namespace?: string;
|
|
258
258
|
resolverPayload?: Record<string, unknown>;
|
|
259
|
+
/**
|
|
260
|
+
* Merged (not replaced) into the resolved row's `metadata` in the same atomic
|
|
261
|
+
* UPDATE — and only on the winning resolve. Records "what actually happened"
|
|
262
|
+
* into the GIN-indexed (`@>`-queryable) surface alongside the creation metadata.
|
|
263
|
+
* Distinct from `resolverPayload`, which is delivered to the waiting workflow as
|
|
264
|
+
* `condition()`'s return value and is not GIN-indexed.
|
|
265
|
+
*/
|
|
266
|
+
metadata?: Record<string, unknown>;
|
|
259
267
|
}
|
|
260
268
|
export interface ResolveByMetadataParams {
|
|
261
269
|
key: string;
|
|
@@ -263,6 +271,12 @@ export interface ResolveByMetadataParams {
|
|
|
263
271
|
namespace?: string;
|
|
264
272
|
resolverPayload?: Record<string, unknown>;
|
|
265
273
|
roles?: string[];
|
|
274
|
+
/**
|
|
275
|
+
* Merge patch applied to the matched row's `metadata` (shallow, not replaced) in
|
|
276
|
+
* the same atomic UPDATE. Note this is the resolution patch — distinct from the
|
|
277
|
+
* `key`/`value` selector used to find the row. See {@link ResolveEscalationParams.metadata}.
|
|
278
|
+
*/
|
|
279
|
+
metadata?: Record<string, unknown>;
|
|
266
280
|
}
|
|
267
281
|
export interface EscalateToRoleParams {
|
|
268
282
|
id: string;
|
|
@@ -289,6 +303,11 @@ export interface ResolveManyParams {
|
|
|
289
303
|
ids: string[];
|
|
290
304
|
namespace?: string;
|
|
291
305
|
resolverPayload?: Record<string, unknown>;
|
|
306
|
+
/**
|
|
307
|
+
* Merged (not replaced) into every winning (still-pending) row's `metadata` in
|
|
308
|
+
* the single bulk UPDATE. See {@link ResolveEscalationParams.metadata}.
|
|
309
|
+
*/
|
|
310
|
+
metadata?: Record<string, unknown>;
|
|
292
311
|
}
|
|
293
312
|
/**
|
|
294
313
|
* Full-fidelity migration params. Extends `CreateEscalationParams` with:
|