@hotmeshio/hotmesh 0.24.1 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.24.1",
3
+ "version": "0.25.0",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -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':
@@ -2051,45 +2051,65 @@ class PostgresStoreService extends __1.StoreService {
2051
2051
  queryParams.push(namespace);
2052
2052
  if (roles?.length)
2053
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.
2054
2065
  const result = await this.pgClient.query(`
2055
- WITH base AS (
2056
- SELECT * FROM public.hmsh_escalations
2057
- WHERE true ${nsClause} ${rolClause}
2058
- ),
2059
- totals AS (
2060
- SELECT
2061
- COUNT(*) FILTER (WHERE status = 'pending')::int AS pending,
2062
- COUNT(*) FILTER (WHERE status = 'pending'
2063
- AND assigned_to IS NOT NULL
2064
- AND assigned_until IS NOT NULL AND assigned_until > NOW())::int AS claimed,
2065
- COUNT(*) FILTER (WHERE created_at >= NOW() - ($1 * INTERVAL '1 hour'))::int AS created,
2066
- COUNT(*) FILTER (WHERE resolved_at >= NOW() - ($1 * INTERVAL '1 hour'))::int AS resolved
2067
- FROM base
2068
- ),
2069
- by_role AS (
2066
+ WITH pending_by_role AS (
2070
2067
  SELECT role,
2071
- COUNT(*) FILTER (WHERE status = 'pending')::int AS pending,
2072
- COUNT(*) FILTER (WHERE status = 'pending'
2073
- AND assigned_to IS NOT NULL
2068
+ COUNT(*)::int AS pending,
2069
+ COUNT(*) FILTER (WHERE assigned_to IS NOT NULL
2074
2070
  AND assigned_until IS NOT NULL AND assigned_until > NOW())::int AS claimed
2075
- FROM base WHERE role IS NOT NULL
2071
+ FROM public.hmsh_escalations
2072
+ WHERE status = 'pending' ${nsClause} ${rolClause}
2076
2073
  GROUP BY role
2077
2074
  ),
2078
- by_type AS (
2075
+ pending_by_type AS (
2079
2076
  SELECT type,
2080
- COUNT(*) FILTER (WHERE status = 'pending')::int AS pending,
2081
- COUNT(*) FILTER (WHERE status = 'pending'
2082
- AND assigned_to IS NOT NULL
2083
- AND assigned_until IS NOT NULL AND assigned_until > NOW())::int AS claimed,
2084
- COUNT(*) FILTER (WHERE resolved_at >= NOW() - ($1 * INTERVAL '1 hour'))::int AS resolved
2085
- FROM base WHERE type IS NOT NULL
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}
2086
2094
  GROUP BY type
2087
2095
  )
2088
2096
  SELECT
2089
- t.pending, t.claimed, t.created, t.resolved,
2090
- COALESCE((SELECT json_agg(json_build_object('role',role,'pending',pending,'claimed',claimed)) FROM by_role), '[]'::json) AS by_role,
2091
- COALESCE((SELECT json_agg(json_build_object('type',type,'pending',pending,'claimed',claimed,'resolved',resolved)) FROM by_type), '[]'::json) AS by_type
2092
- FROM totals t
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
2093
2113
  `, queryParams);
2094
2114
  const row = result.rows[0];
2095
2115
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.24.1",
3
+ "version": "0.25.0",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",