@hotmeshio/hotmesh 0.22.2 → 0.22.4
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 +1 -0
- package/build/package.json +3 -1
- package/build/services/activities/hook.js +34 -5
- package/build/services/durable/client.d.ts +12 -0
- package/build/services/durable/client.js +13 -1
- package/build/services/durable/worker.d.ts +10 -0
- package/build/services/durable/worker.js +33 -0
- package/build/services/escalations/client.d.ts +69 -12
- package/build/services/escalations/client.js +137 -19
- package/build/services/hotmesh/index.d.ts +9 -0
- package/build/services/hotmesh/index.js +37 -0
- package/build/services/store/providers/postgres/kvtables.js +165 -6
- package/build/services/store/providers/postgres/postgres.d.ts +12 -16
- package/build/services/store/providers/postgres/postgres.js +279 -207
- package/build/types/durable.d.ts +11 -0
- package/build/types/hmsh_escalations.d.ts +71 -0
- package/build/types/hotmesh.d.ts +17 -0
- package/build/types/index.d.ts +1 -0
- package/build/types/system_events.d.ts +178 -0
- package/build/types/system_events.js +104 -0
- package/index.ts +1 -0
- package/package.json +3 -2
|
@@ -170,6 +170,13 @@ class HotMesh {
|
|
|
170
170
|
* functions that consume messages from Postgres streams — they can
|
|
171
171
|
* run on the same process or on entirely separate servers.
|
|
172
172
|
*
|
|
173
|
+
* Pass `config.events` to receive lifecycle events from this engine:
|
|
174
|
+
* - `system.engine.{appId}.started` — fires when init completes.
|
|
175
|
+
* - `system.engine.{appId}.deployed` — fires after schema deploy.
|
|
176
|
+
* - `system.engine.{appId}.stopped` — fires when `stop()` is called.
|
|
177
|
+
* - `system.escalation.{id}.created` — fires from the hook Leg1 path
|
|
178
|
+
* (YAML `escalation:` block) when the escalation row commits.
|
|
179
|
+
*
|
|
173
180
|
* @param config - Engine connection, worker definitions, app ID, and options.
|
|
174
181
|
* @returns A running HotMesh instance joined to the quorum.
|
|
175
182
|
*/
|
|
@@ -196,6 +203,24 @@ class HotMesh {
|
|
|
196
203
|
});
|
|
197
204
|
}
|
|
198
205
|
await Init.doWork(instance, config, instance.logger);
|
|
206
|
+
// Thread events.publish to the store (used by the hook Leg1 path and deploy).
|
|
207
|
+
if (config.events?.publish && instance.engine?.store) {
|
|
208
|
+
instance.engine.store.eventsPublish = config.events.publish;
|
|
209
|
+
}
|
|
210
|
+
// Retain for stop() lifecycle event.
|
|
211
|
+
if (config.events?.publish) {
|
|
212
|
+
instance._eventsPublish = config.events.publish;
|
|
213
|
+
const ts = new Date().toISOString();
|
|
214
|
+
const event = {
|
|
215
|
+
event_id: `${config.appId}:started:${ts}`,
|
|
216
|
+
type: `system.engine.${config.appId}.started`,
|
|
217
|
+
ts,
|
|
218
|
+
namespace: instance.namespace,
|
|
219
|
+
app_id: config.appId,
|
|
220
|
+
data: { appId: config.appId, guid: instance.guid },
|
|
221
|
+
};
|
|
222
|
+
void Promise.resolve(config.events.publish(event)).catch(() => { });
|
|
223
|
+
}
|
|
199
224
|
return instance;
|
|
200
225
|
}
|
|
201
226
|
/**
|
|
@@ -489,6 +514,18 @@ class HotMesh {
|
|
|
489
514
|
this.workers?.forEach((worker) => {
|
|
490
515
|
worker.stop();
|
|
491
516
|
});
|
|
517
|
+
if (this._eventsPublish) {
|
|
518
|
+
const ts = new Date().toISOString();
|
|
519
|
+
const event = {
|
|
520
|
+
event_id: `${this.appId}:stopped:${ts}`,
|
|
521
|
+
type: `system.engine.${this.appId}.stopped`,
|
|
522
|
+
ts,
|
|
523
|
+
namespace: this.namespace,
|
|
524
|
+
app_id: this.appId,
|
|
525
|
+
data: { appId: this.appId, guid: this.guid },
|
|
526
|
+
};
|
|
527
|
+
void Promise.resolve(this._eventsPublish(event)).catch(() => { });
|
|
528
|
+
}
|
|
492
529
|
}
|
|
493
530
|
/**
|
|
494
531
|
* @private
|
|
@@ -71,6 +71,18 @@ const KVTables = (context) => ({
|
|
|
71
71
|
await client.release();
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
// Fire system.engine.{appId}.deployed post-deploy (best-effort).
|
|
75
|
+
if (context.eventsPublish) {
|
|
76
|
+
const ts = new Date().toISOString();
|
|
77
|
+
void Promise.resolve(context.eventsPublish({
|
|
78
|
+
event_id: `${appName}:deployed:${ts}`,
|
|
79
|
+
type: `system.engine.${appName}.deployed`,
|
|
80
|
+
ts,
|
|
81
|
+
namespace: appName,
|
|
82
|
+
app_id: appName,
|
|
83
|
+
data: { appId: appName },
|
|
84
|
+
})).catch(() => { });
|
|
85
|
+
}
|
|
74
86
|
},
|
|
75
87
|
getAdvisoryLockId(appName) {
|
|
76
88
|
return this.hashStringToInt(appName);
|
|
@@ -166,6 +178,145 @@ const KVTables = (context) => ({
|
|
|
166
178
|
ON ${jobsTable} (key) WHERE is_live;
|
|
167
179
|
`);
|
|
168
180
|
}
|
|
181
|
+
// v0.22.0: origin_id, parent_id on per-app jobs table (workflow lineage).
|
|
182
|
+
// ADD COLUMN IF NOT EXISTS on a nullable column with no DEFAULT is catalog-only
|
|
183
|
+
// in PG11+ — no table rewrite, brief AccessExclusiveLock only.
|
|
184
|
+
await client.query(`
|
|
185
|
+
ALTER TABLE ${jobsTable} ADD COLUMN IF NOT EXISTS origin_id TEXT;
|
|
186
|
+
`);
|
|
187
|
+
await client.query(`
|
|
188
|
+
ALTER TABLE ${jobsTable} ADD COLUMN IF NOT EXISTS parent_id TEXT;
|
|
189
|
+
`);
|
|
190
|
+
// Partial indexes — only cover the opt-in rows so build is near-instant.
|
|
191
|
+
const { rows: originIdxRows } = await client.query(`SELECT 1 FROM pg_indexes WHERE indexname = $1 AND schemaname = $2 LIMIT 1`, [`idx_${schemaName}_jobs_origin`, schemaName]);
|
|
192
|
+
if (originIdxRows.length === 0) {
|
|
193
|
+
await client.query(`
|
|
194
|
+
CREATE INDEX IF NOT EXISTS idx_${schemaName}_jobs_origin
|
|
195
|
+
ON ${jobsTable}(origin_id) WHERE origin_id IS NOT NULL;
|
|
196
|
+
`);
|
|
197
|
+
}
|
|
198
|
+
const { rows: parentIdxRows } = await client.query(`SELECT 1 FROM pg_indexes WHERE indexname = $1 AND schemaname = $2 LIMIT 1`, [`idx_${schemaName}_jobs_parent`, schemaName]);
|
|
199
|
+
if (parentIdxRows.length === 0) {
|
|
200
|
+
await client.query(`
|
|
201
|
+
CREATE INDEX IF NOT EXISTS idx_${schemaName}_jobs_parent
|
|
202
|
+
ON ${jobsTable}(parent_id) WHERE parent_id IS NOT NULL;
|
|
203
|
+
`);
|
|
204
|
+
}
|
|
205
|
+
// ─── v0.22.0+: public.hmsh_escalations global table migrations ───────────
|
|
206
|
+
// Use the fixed advisory XACT lock so concurrent deployments of DIFFERENT
|
|
207
|
+
// appIds (each holding their own per-appId session lock) don't race on the
|
|
208
|
+
// shared public table DDL. Transaction-level lock auto-releases on commit/rollback.
|
|
209
|
+
await client.query('BEGIN');
|
|
210
|
+
await client.query('SELECT pg_advisory_xact_lock($1)', [0x484D5348]);
|
|
211
|
+
// v0.22.0: create hmsh_escalations if missing (upgrade from pre-0.22.x).
|
|
212
|
+
// Column list matches createTables() exactly; task_id is NOT here because
|
|
213
|
+
// the ADD COLUMN below is the canonical way to add it to both fresh and
|
|
214
|
+
// existing tables in a single idempotent statement.
|
|
215
|
+
await client.query(`
|
|
216
|
+
CREATE TABLE IF NOT EXISTS public.hmsh_escalations (
|
|
217
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
218
|
+
namespace TEXT NOT NULL,
|
|
219
|
+
app_id TEXT NOT NULL,
|
|
220
|
+
signal_key TEXT,
|
|
221
|
+
topic TEXT,
|
|
222
|
+
workflow_id TEXT,
|
|
223
|
+
task_queue TEXT,
|
|
224
|
+
workflow_type TEXT,
|
|
225
|
+
type TEXT,
|
|
226
|
+
subtype TEXT,
|
|
227
|
+
entity TEXT,
|
|
228
|
+
description TEXT,
|
|
229
|
+
role TEXT,
|
|
230
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
231
|
+
priority INT NOT NULL DEFAULT 5,
|
|
232
|
+
assigned_to TEXT,
|
|
233
|
+
assigned_until TIMESTAMPTZ,
|
|
234
|
+
claimed_at TIMESTAMPTZ,
|
|
235
|
+
claim_expires_at TIMESTAMPTZ,
|
|
236
|
+
resolved_at TIMESTAMPTZ,
|
|
237
|
+
escalation_payload JSONB,
|
|
238
|
+
resolver_payload JSONB,
|
|
239
|
+
envelope JSONB,
|
|
240
|
+
metadata JSONB,
|
|
241
|
+
origin_id TEXT,
|
|
242
|
+
parent_id TEXT,
|
|
243
|
+
initiated_by TEXT,
|
|
244
|
+
created_by TEXT,
|
|
245
|
+
milestones JSONB NOT NULL DEFAULT '[]',
|
|
246
|
+
trace_id TEXT,
|
|
247
|
+
span_id TEXT,
|
|
248
|
+
expires_at TIMESTAMPTZ,
|
|
249
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
250
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
251
|
+
);
|
|
252
|
+
`);
|
|
253
|
+
// v0.22.3: task_id column — catalog-only ADD COLUMN (nullable TEXT, no DEFAULT,
|
|
254
|
+
// no table rewrite). IF NOT EXISTS makes this idempotent across all upgrade paths.
|
|
255
|
+
await client.query(`
|
|
256
|
+
ALTER TABLE public.hmsh_escalations ADD COLUMN IF NOT EXISTS task_id TEXT;
|
|
257
|
+
`);
|
|
258
|
+
// Ensure signal_key unique index exists.
|
|
259
|
+
await client.query(`
|
|
260
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_hmsh_esc_signal_key
|
|
261
|
+
ON public.hmsh_escalations(namespace, app_id, signal_key)
|
|
262
|
+
WHERE signal_key IS NOT NULL;
|
|
263
|
+
`);
|
|
264
|
+
// v0.22.0: idx_hmsh_esc_available — predicate must be status='pending'.
|
|
265
|
+
// Fresh 0.22.0 installs may have this with the correct predicate already;
|
|
266
|
+
// ensure it exists.
|
|
267
|
+
await client.query(`
|
|
268
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_available
|
|
269
|
+
ON public.hmsh_escalations(namespace, app_id, role, priority ASC, created_at ASC)
|
|
270
|
+
WHERE status = 'pending';
|
|
271
|
+
`);
|
|
272
|
+
await client.query(`
|
|
273
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_available_expiry
|
|
274
|
+
ON public.hmsh_escalations(namespace, app_id, role, assigned_until, created_at DESC);
|
|
275
|
+
`);
|
|
276
|
+
// v0.22.2: idx_hmsh_esc_assigned predicate changed from status='claimed' to
|
|
277
|
+
// status='pending' to match the implicit claim model. If the old predicate
|
|
278
|
+
// is present, drop and recreate so claimEscalation / list queries use it.
|
|
279
|
+
const { rows: assignedDefRows } = await client.query(`
|
|
280
|
+
SELECT indexdef FROM pg_indexes WHERE indexname = 'idx_hmsh_esc_assigned' LIMIT 1
|
|
281
|
+
`);
|
|
282
|
+
if (assignedDefRows.length > 0 &&
|
|
283
|
+
assignedDefRows[0].indexdef.includes("status = 'claimed'")) {
|
|
284
|
+
await client.query(`DROP INDEX IF EXISTS idx_hmsh_esc_assigned;`);
|
|
285
|
+
}
|
|
286
|
+
await client.query(`
|
|
287
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_assigned
|
|
288
|
+
ON public.hmsh_escalations(assigned_to, assigned_until, created_at DESC)
|
|
289
|
+
WHERE status = 'pending' AND assigned_to IS NOT NULL;
|
|
290
|
+
`);
|
|
291
|
+
// v0.22.2: claim_expiry sweeper index is obsolete (releaseExpiredEscalations
|
|
292
|
+
// is a no-op in the implicit-claim model — availability is query-time computed).
|
|
293
|
+
await client.query(`DROP INDEX IF EXISTS idx_hmsh_esc_claim_expiry;`);
|
|
294
|
+
await client.query(`
|
|
295
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_entity
|
|
296
|
+
ON public.hmsh_escalations(namespace, app_id, entity, created_at DESC)
|
|
297
|
+
WHERE entity IS NOT NULL;
|
|
298
|
+
`);
|
|
299
|
+
await client.query(`
|
|
300
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_workflow
|
|
301
|
+
ON public.hmsh_escalations(workflow_id)
|
|
302
|
+
WHERE workflow_id IS NOT NULL;
|
|
303
|
+
`);
|
|
304
|
+
await client.query(`
|
|
305
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_origin
|
|
306
|
+
ON public.hmsh_escalations(origin_id)
|
|
307
|
+
WHERE origin_id IS NOT NULL;
|
|
308
|
+
`);
|
|
309
|
+
await client.query(`
|
|
310
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_metadata
|
|
311
|
+
ON public.hmsh_escalations USING GIN(metadata jsonb_path_ops);
|
|
312
|
+
`);
|
|
313
|
+
// v0.22.3: task_id partial index.
|
|
314
|
+
await client.query(`
|
|
315
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_task
|
|
316
|
+
ON public.hmsh_escalations(task_id)
|
|
317
|
+
WHERE task_id IS NOT NULL;
|
|
318
|
+
`);
|
|
319
|
+
await client.query('COMMIT');
|
|
169
320
|
},
|
|
170
321
|
async createTables(client, appName) {
|
|
171
322
|
try {
|
|
@@ -253,16 +404,15 @@ const KVTables = (context) => ({
|
|
|
253
404
|
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_available_expiry
|
|
254
405
|
ON public.hmsh_escalations(namespace, app_id, role, assigned_until, created_at DESC);
|
|
255
406
|
`);
|
|
407
|
+
// idx_hmsh_esc_assigned: implicit-claim model uses status='pending', not 'claimed'
|
|
408
|
+
await client.query(`DROP INDEX IF EXISTS idx_hmsh_esc_assigned;`);
|
|
256
409
|
await client.query(`
|
|
257
410
|
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_assigned
|
|
258
411
|
ON public.hmsh_escalations(assigned_to, assigned_until, created_at DESC)
|
|
259
|
-
WHERE status = '
|
|
260
|
-
`);
|
|
261
|
-
await client.query(`
|
|
262
|
-
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_claim_expiry
|
|
263
|
-
ON public.hmsh_escalations(claim_expires_at)
|
|
264
|
-
WHERE status = 'claimed';
|
|
412
|
+
WHERE status = 'pending' AND assigned_to IS NOT NULL;
|
|
265
413
|
`);
|
|
414
|
+
// idx_hmsh_esc_claim_expiry: no longer used (releaseExpiredEscalations is a no-op)
|
|
415
|
+
await client.query(`DROP INDEX IF EXISTS idx_hmsh_esc_claim_expiry;`);
|
|
266
416
|
await client.query(`
|
|
267
417
|
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_entity
|
|
268
418
|
ON public.hmsh_escalations(namespace, app_id, entity, created_at DESC)
|
|
@@ -281,6 +431,15 @@ const KVTables = (context) => ({
|
|
|
281
431
|
await client.query(`
|
|
282
432
|
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_metadata
|
|
283
433
|
ON public.hmsh_escalations USING GIN(metadata jsonb_path_ops);
|
|
434
|
+
`);
|
|
435
|
+
// task_id: nullable passthrough column for downstream compat (no FK)
|
|
436
|
+
await client.query(`
|
|
437
|
+
ALTER TABLE public.hmsh_escalations ADD COLUMN IF NOT EXISTS task_id TEXT;
|
|
438
|
+
`);
|
|
439
|
+
await client.query(`
|
|
440
|
+
CREATE INDEX IF NOT EXISTS idx_hmsh_esc_task
|
|
441
|
+
ON public.hmsh_escalations(task_id)
|
|
442
|
+
WHERE task_id IS NOT NULL;
|
|
284
443
|
`);
|
|
285
444
|
break;
|
|
286
445
|
case 'relational_connection':
|
|
@@ -19,6 +19,8 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
|
|
|
19
19
|
pgClient: PostgresClientType;
|
|
20
20
|
kvTables: ReturnType<typeof KVTables>;
|
|
21
21
|
isScout: boolean;
|
|
22
|
+
/** Set by HotMesh.init() when `events.publish` is configured. Used by hook.ts Leg1 path. */
|
|
23
|
+
eventsPublish?: (event: import('../../../../types/system_events').SystemEvent) => void | Promise<void>;
|
|
22
24
|
transact(): ProviderTransaction;
|
|
23
25
|
constructor(storeClient: ProviderClient);
|
|
24
26
|
init(namespace: string, appId: string, logger: ILogger, guid?: string, role?: string): Promise<HotMeshApps>;
|
|
@@ -246,6 +248,7 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
|
|
|
246
248
|
createEscalationForMigration(params: import('../../../../types/hmsh_escalations').MigrateEscalationParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
|
|
247
249
|
getEscalation(id: string, namespace?: string): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
|
|
248
250
|
getEscalationBySignalKey(signalKey: string, namespace?: string): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
|
|
251
|
+
private _escalationFilterConditions;
|
|
249
252
|
listEscalations(params?: import('../../../../types/hmsh_escalations').ListEscalationsParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry[]>;
|
|
250
253
|
countEscalations(params?: import('../../../../types/hmsh_escalations').ListEscalationsParams): Promise<number>;
|
|
251
254
|
claimEscalation(params: import('../../../../types/hmsh_escalations').ClaimEscalationParams): Promise<import('../../../../types/hmsh_escalations').ClaimEscalationResult>;
|
|
@@ -259,26 +262,19 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
|
|
|
259
262
|
signalKey?: string | null;
|
|
260
263
|
topic?: string | null;
|
|
261
264
|
}>;
|
|
262
|
-
/**
|
|
263
|
-
* Queues the escalation UPDATE into an existing transaction without executing it.
|
|
264
|
-
* Used by client.ts resolve() to commit the status change atomically with signal delivery.
|
|
265
|
-
*/
|
|
266
|
-
queueResolveEscalation(params: {
|
|
267
|
-
id: string;
|
|
268
|
-
namespace?: string;
|
|
269
|
-
resolverPayload?: Record<string, unknown>;
|
|
270
|
-
}, transaction: {
|
|
271
|
-
addCommand: (sql: string, params: any[], returnType: string) => any;
|
|
272
|
-
}): void;
|
|
273
|
-
/**
|
|
274
|
-
* Finds the highest-priority pending or claimed escalation matching the given metadata filter.
|
|
275
|
-
* Used by client.ts resolveByMetadata() to get routing info before building the atomic transaction.
|
|
276
|
-
*/
|
|
277
|
-
findEscalationByMetadata(key: string, value: string, roles: string[] | null, namespace?: string): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
|
|
278
265
|
cancelEscalation(id: string, namespace?: string): Promise<import('../../../../types/hmsh_escalations').CancelEscalationResult>;
|
|
279
266
|
escalateEscalationToRole(params: import('../../../../types/hmsh_escalations').EscalateToRoleParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
|
|
280
267
|
updateEscalation(params: import('../../../../types/hmsh_escalations').UpdateEscalationParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
|
|
281
268
|
appendEscalationMilestones(params: import('../../../../types/hmsh_escalations').AppendMilestonesParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
|
|
269
|
+
claimManyEscalations(params: import('../../../../types/hmsh_escalations').ClaimManyParams): Promise<{
|
|
270
|
+
entries: import('../../../../types/hmsh_escalations').EscalationEntry[];
|
|
271
|
+
skipped: number;
|
|
272
|
+
}>;
|
|
273
|
+
escalateManyEscalationsToRole(params: import('../../../../types/hmsh_escalations').EscalateManyToRoleParams): Promise<number>;
|
|
274
|
+
updateManyEscalationsPriority(params: import('../../../../types/hmsh_escalations').UpdateManyPriorityParams): Promise<number>;
|
|
275
|
+
resolveManyEscalations(params: import('../../../../types/hmsh_escalations').ResolveManyParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry[]>;
|
|
276
|
+
escalationStats(params?: import('../../../../types/hmsh_escalations').StatsEscalationsParams): Promise<import('../../../../types/hmsh_escalations').EscalationStats>;
|
|
277
|
+
listDistinctEscalationTypes(namespace?: string): Promise<string[]>;
|
|
282
278
|
releaseExpiredEscalations(_namespace?: string): Promise<number>;
|
|
283
279
|
}
|
|
284
280
|
export { PostgresStoreService };
|