@hotmeshio/hotmesh 0.22.0 → 0.22.1
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/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 +36 -1
- package/build/services/durable/client.js +99 -50
- package/build/services/durable/index.d.ts +7 -1
- package/build/services/durable/index.js +6 -0
- package/build/services/store/providers/postgres/postgres.d.ts +7 -0
- package/build/services/store/providers/postgres/postgres.js +62 -0
- package/build/types/hmsh_escalations.d.ts +28 -0
- package/build/types/index.d.ts +1 -1
- package/package.json +9 -3
package/build/modules/utils.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare function getSystemHealth(): Promise<SystemHealth>;
|
|
|
13
13
|
export declare function deepCopy<T>(obj: T): T;
|
|
14
14
|
export declare function deterministicRandom(seed: number): number;
|
|
15
15
|
export declare function guid(size?: number): string;
|
|
16
|
+
/** Returns a standard RFC 4122 v4 UUID (e.g. for use as a DB primary key). */
|
|
17
|
+
export declare function uuid(): string;
|
|
16
18
|
export declare function sleepFor(ms: number): Promise<void>;
|
|
17
19
|
export declare function sleepImmediate(): Promise<void>;
|
|
18
20
|
export declare function XSleepFor(ms: number): {
|
package/build/modules/utils.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.arrayToHash = exports.isStreamMessage = exports.parseStreamMessage = exports.normalizeRetryPolicy = exports.s = exports.isValidCron = exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.matchesStatus = exports.matchesStatusCode = exports.polyfill = exports.identifyProvider = exports.XSleepFor = exports.sleepImmediate = exports.sleepFor = exports.guid = exports.deterministicRandom = exports.deepCopy = exports.getSystemHealth = exports.hashOptions = void 0;
|
|
6
|
+
exports.arrayToHash = exports.isStreamMessage = exports.parseStreamMessage = exports.normalizeRetryPolicy = exports.s = exports.isValidCron = exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.matchesStatus = exports.matchesStatusCode = exports.polyfill = exports.identifyProvider = exports.XSleepFor = exports.sleepImmediate = exports.sleepFor = exports.uuid = exports.guid = exports.deterministicRandom = exports.deepCopy = exports.getSystemHealth = exports.hashOptions = void 0;
|
|
7
7
|
const os_1 = __importDefault(require("os"));
|
|
8
8
|
const crypto_1 = require("crypto");
|
|
9
9
|
const cron_parser_1 = require("cron-parser");
|
|
@@ -47,6 +47,11 @@ function guid(size = enums_1.HMSH_GUID_SIZE) {
|
|
|
47
47
|
return `H` + (0, nanoid_1.nanoid)(size);
|
|
48
48
|
}
|
|
49
49
|
exports.guid = guid;
|
|
50
|
+
/** Returns a standard RFC 4122 v4 UUID (e.g. for use as a DB primary key). */
|
|
51
|
+
function uuid() {
|
|
52
|
+
return (0, crypto_1.randomUUID)();
|
|
53
|
+
}
|
|
54
|
+
exports.uuid = uuid;
|
|
50
55
|
async function sleepFor(ms) {
|
|
51
56
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
52
57
|
}
|
package/build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
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,12 @@
|
|
|
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
|
+
"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",
|
|
90
|
+
"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",
|
|
91
|
+
"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",
|
|
92
|
+
"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",
|
|
93
|
+
"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
94
|
},
|
|
90
95
|
"keywords": [
|
|
91
96
|
"Invisible Infrastructure",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HotMesh } from '../hotmesh';
|
|
2
2
|
import { ClientConfig, ClientWorkflow, Connection, WorkflowOptions } from '../../types/durable';
|
|
3
|
-
import { EscalationEntry, ClaimEscalationResult, ClaimByMetadataResult, ReleaseEscalationResult, ResolveEscalationResult, CancelEscalationResult, ListEscalationsParams, CreateEscalationParams, UpdateEscalationParams, AppendMilestonesParams, ClaimEscalationParams, ClaimByMetadataParams, ReleaseEscalationParams, ResolveEscalationParams, ResolveByMetadataParams, EscalateToRoleParams } from '../../types/hmsh_escalations';
|
|
3
|
+
import { EscalationEntry, ClaimEscalationResult, ClaimByMetadataResult, ReleaseEscalationResult, ResolveEscalationResult, CancelEscalationResult, ListEscalationsParams, CreateEscalationParams, UpdateEscalationParams, AppendMilestonesParams, ClaimEscalationParams, ClaimByMetadataParams, ReleaseEscalationParams, ResolveEscalationParams, ResolveByMetadataParams, EscalateToRoleParams, MigrateEscalationParams } from '../../types/hmsh_escalations';
|
|
4
4
|
/**
|
|
5
5
|
* Workflow client. Starts workflows, sends signals, and reads results.
|
|
6
6
|
*
|
|
@@ -320,6 +320,26 @@ export declare class ClientService {
|
|
|
320
320
|
* ```
|
|
321
321
|
*/
|
|
322
322
|
resolveByMetadata: (params: ResolveByMetadataParams, namespace?: string) => Promise<ResolveEscalationResult>;
|
|
323
|
+
/**
|
|
324
|
+
* Full-fidelity migration: inserts an escalation row preserving the original
|
|
325
|
+
* UUID and all lifecycle state. Returns the inserted row, or `null` if the
|
|
326
|
+
* UUID already exists (idempotent — safe to call multiple times with the same
|
|
327
|
+
* `params.id`). Use this to migrate rows from a legacy escalation table to
|
|
328
|
+
* `hmsh_escalations` without losing original IDs or state.
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* ```typescript
|
|
332
|
+
* const entry = await client.escalations.migrate({
|
|
333
|
+
* id: 'original-uuid',
|
|
334
|
+
* status: 'resolved',
|
|
335
|
+
* resolvedAt: new Date('2025-01-01'),
|
|
336
|
+
* type: 'order-approval',
|
|
337
|
+
* role: 'approver',
|
|
338
|
+
* });
|
|
339
|
+
* // null on subsequent calls with the same id — idempotent
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
migrate: (params: MigrateEscalationParams, namespace?: string) => Promise<EscalationEntry | null>;
|
|
323
343
|
/**
|
|
324
344
|
* Releases all claimed escalations whose `claim_expires_at` has lapsed,
|
|
325
345
|
* returning them to `pending` so they can be claimed again. Returns the
|
|
@@ -334,6 +354,21 @@ export declare class ClientService {
|
|
|
334
354
|
*/
|
|
335
355
|
releaseExpired: (namespace?: string) => Promise<number>;
|
|
336
356
|
};
|
|
357
|
+
/**
|
|
358
|
+
* Delivers a signal to the registered escalation topic.
|
|
359
|
+
*
|
|
360
|
+
* When the topic is known (stored at condition() time), we deliver only to that
|
|
361
|
+
* topic. This avoids writing a stale pending entry to the alternate stream, which
|
|
362
|
+
* would interfere with concurrent consumers in other workflows or tests.
|
|
363
|
+
*
|
|
364
|
+
* When topic is null (legacy/standalone rows with no registered topic), we try
|
|
365
|
+
* both wfs.signal and wfs.wait unconditionally — engine.signal() on the wrong
|
|
366
|
+
* topic stores pending without throwing, so a sequential fallback would skip
|
|
367
|
+
* the second topic and leave single-condition workflows permanently suspended.
|
|
368
|
+
*
|
|
369
|
+
* @private
|
|
370
|
+
*/
|
|
371
|
+
private _deliverEscalationSignal;
|
|
337
372
|
/**
|
|
338
373
|
* @private
|
|
339
374
|
*/
|
|
@@ -528,33 +528,26 @@ class ClientService {
|
|
|
528
528
|
const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
|
|
529
529
|
const hotMeshClient = await this.getHotMeshClient(null, ns);
|
|
530
530
|
const store = hotMeshClient.engine.store;
|
|
531
|
-
//
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
if (
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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 */ }
|
|
531
|
+
// store.resolveEscalation() uses FOR UPDATE inside its CTE, serializing concurrent
|
|
532
|
+
// callers at the DB level. The second caller blocks on the row lock, reads
|
|
533
|
+
// 'already-resolved' after the first commits, and returns — no signal is queued.
|
|
534
|
+
// This eliminates the double-signal race that the previous KVTransaction approach
|
|
535
|
+
// had: two concurrent callers could both pass the unlocked getEscalation() read,
|
|
536
|
+
// both queue signal INSERTs, and both commit them — even though only one UPDATE won.
|
|
537
|
+
const dbResult = await store.resolveEscalation({
|
|
538
|
+
id: params.id,
|
|
539
|
+
resolverPayload: params.resolverPayload,
|
|
540
|
+
// namespace intentionally omitted — UUID lookup; passing ns would miss rows
|
|
541
|
+
// stored under namespace 'hmsh' (the engine default) when ns = APP_ID = 'durable'.
|
|
542
|
+
});
|
|
543
|
+
if (!dbResult.ok)
|
|
544
|
+
return dbResult;
|
|
545
|
+
if (dbResult.signalKey) {
|
|
546
|
+
const signalPayload = { id: dbResult.signalKey, data: params.resolverPayload ?? {} };
|
|
547
|
+
const delivered = await this._deliverEscalationSignal(ns, dbResult.topic, signalPayload);
|
|
548
|
+
if (!delivered)
|
|
549
|
+
return { ok: false, reason: 'signal-failed' };
|
|
555
550
|
}
|
|
556
|
-
store.queueResolveEscalation({ id: params.id, namespace: existing.namespace ?? ns, resolverPayload: params.resolverPayload }, txn);
|
|
557
|
-
await txn.exec();
|
|
558
551
|
return { ok: true };
|
|
559
552
|
},
|
|
560
553
|
/**
|
|
@@ -575,33 +568,49 @@ class ClientService {
|
|
|
575
568
|
const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
|
|
576
569
|
const hotMeshClient = await this.getHotMeshClient(null, ns);
|
|
577
570
|
const store = hotMeshClient.engine.store;
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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 */ }
|
|
571
|
+
// Same FOR UPDATE CTE serialization as resolve(). Metadata filter selects
|
|
572
|
+
// the highest-priority matching row; the lock prevents concurrent callers
|
|
573
|
+
// from both resolving it.
|
|
574
|
+
const dbResult = await store.resolveEscalationByMetadata({
|
|
575
|
+
key: params.key,
|
|
576
|
+
value: params.value,
|
|
577
|
+
resolverPayload: params.resolverPayload,
|
|
578
|
+
roles: params.roles,
|
|
579
|
+
});
|
|
580
|
+
if (!dbResult.ok)
|
|
581
|
+
return dbResult;
|
|
582
|
+
if (dbResult.signalKey) {
|
|
583
|
+
const signalPayload = { id: dbResult.signalKey, data: params.resolverPayload ?? {} };
|
|
584
|
+
const delivered = await this._deliverEscalationSignal(ns, dbResult.topic, signalPayload);
|
|
585
|
+
if (!delivered)
|
|
586
|
+
return { ok: false, reason: 'signal-failed' };
|
|
600
587
|
}
|
|
601
|
-
store.queueResolveEscalation({ id: existing.id, namespace: existing.namespace ?? ns, resolverPayload: params.resolverPayload }, txn);
|
|
602
|
-
await txn.exec();
|
|
603
588
|
return { ok: true };
|
|
604
589
|
},
|
|
590
|
+
/**
|
|
591
|
+
* Full-fidelity migration: inserts an escalation row preserving the original
|
|
592
|
+
* UUID and all lifecycle state. Returns the inserted row, or `null` if the
|
|
593
|
+
* UUID already exists (idempotent — safe to call multiple times with the same
|
|
594
|
+
* `params.id`). Use this to migrate rows from a legacy escalation table to
|
|
595
|
+
* `hmsh_escalations` without losing original IDs or state.
|
|
596
|
+
*
|
|
597
|
+
* @example
|
|
598
|
+
* ```typescript
|
|
599
|
+
* const entry = await client.escalations.migrate({
|
|
600
|
+
* id: 'original-uuid',
|
|
601
|
+
* status: 'resolved',
|
|
602
|
+
* resolvedAt: new Date('2025-01-01'),
|
|
603
|
+
* type: 'order-approval',
|
|
604
|
+
* role: 'approver',
|
|
605
|
+
* });
|
|
606
|
+
* // null on subsequent calls with the same id — idempotent
|
|
607
|
+
* ```
|
|
608
|
+
*/
|
|
609
|
+
migrate: async (params, namespace) => {
|
|
610
|
+
const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
|
|
611
|
+
const hotMeshClient = await this.getHotMeshClient(null, ns);
|
|
612
|
+
return hotMeshClient.engine.store.createEscalationForMigration(params);
|
|
613
|
+
},
|
|
605
614
|
/**
|
|
606
615
|
* Releases all claimed escalations whose `claim_expires_at` has lapsed,
|
|
607
616
|
* returning them to `pending` so they can be claimed again. Returns the
|
|
@@ -694,6 +703,46 @@ class ClientService {
|
|
|
694
703
|
}
|
|
695
704
|
}
|
|
696
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* Delivers a signal to the registered escalation topic.
|
|
708
|
+
*
|
|
709
|
+
* When the topic is known (stored at condition() time), we deliver only to that
|
|
710
|
+
* topic. This avoids writing a stale pending entry to the alternate stream, which
|
|
711
|
+
* would interfere with concurrent consumers in other workflows or tests.
|
|
712
|
+
*
|
|
713
|
+
* When topic is null (legacy/standalone rows with no registered topic), we try
|
|
714
|
+
* both wfs.signal and wfs.wait unconditionally — engine.signal() on the wrong
|
|
715
|
+
* topic stores pending without throwing, so a sequential fallback would skip
|
|
716
|
+
* the second topic and leave single-condition workflows permanently suspended.
|
|
717
|
+
*
|
|
718
|
+
* @private
|
|
719
|
+
*/
|
|
720
|
+
async _deliverEscalationSignal(ns, topic, signalPayload) {
|
|
721
|
+
if (topic) {
|
|
722
|
+
// Registered topic known — deliver precisely, no stream pollution.
|
|
723
|
+
try {
|
|
724
|
+
const tc = await this.getHotMeshClient(topic, ns);
|
|
725
|
+
await tc.engine.signal(topic, signalPayload, types_1.StreamStatus.SUCCESS, 200);
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
catch { /* topic not currently registered — fall through */ }
|
|
729
|
+
}
|
|
730
|
+
// Topic unknown or primary delivery failed — try both topics unconditionally.
|
|
731
|
+
let delivered = false;
|
|
732
|
+
try {
|
|
733
|
+
const sc = await this.getHotMeshClient(`${ns}.wfs.signal`, ns);
|
|
734
|
+
await sc.engine.signal(`${ns}.wfs.signal`, signalPayload, types_1.StreamStatus.SUCCESS, 200);
|
|
735
|
+
delivered = true;
|
|
736
|
+
}
|
|
737
|
+
catch { /* no collator hook rule for this workflow */ }
|
|
738
|
+
try {
|
|
739
|
+
const wc = await this.getHotMeshClient(`${ns}.wfs.wait`, ns);
|
|
740
|
+
await wc.engine.signal(`${ns}.wfs.wait`, signalPayload, types_1.StreamStatus.SUCCESS, 200);
|
|
741
|
+
delivered = true;
|
|
742
|
+
}
|
|
743
|
+
catch { /* no waiter hook rule for this workflow */ }
|
|
744
|
+
return delivered;
|
|
745
|
+
}
|
|
697
746
|
/**
|
|
698
747
|
* @private
|
|
699
748
|
*/
|
|
@@ -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
|
|
@@ -237,6 +237,13 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
|
|
|
237
237
|
* idempotent re-runs after a crash.
|
|
238
238
|
*/
|
|
239
239
|
addEscalationToTransaction(params: import('../../../../types/hmsh_escalations').CreateEscalationParams, transaction: import('../../../../types/provider').ProviderTransaction): void;
|
|
240
|
+
/**
|
|
241
|
+
* Full-fidelity INSERT for data migration. Preserves the original `id` (UUID),
|
|
242
|
+
* lifecycle state, and timestamps from the source table. Uses
|
|
243
|
+
* `ON CONFLICT (id) DO NOTHING` so re-running a migration batch is safe —
|
|
244
|
+
* rows that already exist are skipped and `null` is returned for them.
|
|
245
|
+
*/
|
|
246
|
+
createEscalationForMigration(params: import('../../../../types/hmsh_escalations').MigrateEscalationParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
|
|
240
247
|
getEscalation(id: string, namespace?: string): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
|
|
241
248
|
getEscalationBySignalKey(signalKey: string, namespace?: string): Promise<import('../../../../types/hmsh_escalations').EscalationEntry | null>;
|
|
242
249
|
listEscalations(params?: import('../../../../types/hmsh_escalations').ListEscalationsParams): Promise<import('../../../../types/hmsh_escalations').EscalationEntry[]>;
|
|
@@ -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;
|
|
@@ -210,3 +210,31 @@ export interface EscalateToRoleParams {
|
|
|
210
210
|
targetRole: string;
|
|
211
211
|
namespace?: string;
|
|
212
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Full-fidelity migration params. Extends `CreateEscalationParams` with:
|
|
215
|
+
* - `id` (required) — preserves the original UUID; no auto-generation
|
|
216
|
+
* - lifecycle state fields (`status`, `assignedTo`, `claimExpiresAt`, …) — carry over
|
|
217
|
+
* the exact state of the migrated row so in-flight escalations land correctly
|
|
218
|
+
* - `createdAt` / `updatedAt` — preserve original timestamps
|
|
219
|
+
*
|
|
220
|
+
* The underlying INSERT uses `ON CONFLICT (id) DO NOTHING`, so calling
|
|
221
|
+
* `migrate()` multiple times with the same ID is safe — subsequent calls
|
|
222
|
+
* return `null` without touching the existing row.
|
|
223
|
+
*/
|
|
224
|
+
export interface MigrateEscalationParams extends CreateEscalationParams {
|
|
225
|
+
/** Required — preserve the original UUID from the source table. */
|
|
226
|
+
id: string;
|
|
227
|
+
status?: 'pending' | 'claimed' | 'resolved' | 'cancelled' | 'expired';
|
|
228
|
+
assignedTo?: string;
|
|
229
|
+
claimExpiresAt?: Date;
|
|
230
|
+
claimedAt?: Date;
|
|
231
|
+
resolvedAt?: Date;
|
|
232
|
+
resolverPayload?: Record<string, unknown>;
|
|
233
|
+
milestones?: Array<{
|
|
234
|
+
name: string;
|
|
235
|
+
value: unknown;
|
|
236
|
+
[key: string]: unknown;
|
|
237
|
+
}>;
|
|
238
|
+
createdAt?: Date;
|
|
239
|
+
updatedAt?: Date;
|
|
240
|
+
}
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
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",
|