@hotmeshio/hotmesh 0.16.0 → 0.16.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/modules/storage.d.ts +1 -0
- package/build/modules/storage.js +2 -1
- package/build/package.json +1 -1
- package/build/services/activities/activity/process.js +24 -6
- package/build/services/pipe/functions/cron.js +4 -1
- package/build/services/store/providers/postgres/postgres.d.ts +15 -7
- package/build/services/store/providers/postgres/postgres.js +70 -61
- package/build/services/task/index.js +1 -1
- package/build/services/virtual/index.d.ts +22 -1
- package/build/services/virtual/index.js +54 -6
- package/build/types/index.d.ts +1 -1
- package/build/types/virtual.d.ts +69 -1
- package/package.json +1 -1
|
@@ -2,3 +2,4 @@
|
|
|
2
2
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
3
3
|
export declare const asyncLocalStorage: AsyncLocalStorage<Map<string, any>>;
|
|
4
4
|
export declare const activityAsyncLocalStorage: AsyncLocalStorage<Map<string, any>>;
|
|
5
|
+
export declare const virtualAsyncLocalStorage: AsyncLocalStorage<Map<string, any>>;
|
package/build/modules/storage.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.activityAsyncLocalStorage = exports.asyncLocalStorage = void 0;
|
|
3
|
+
exports.virtualAsyncLocalStorage = exports.activityAsyncLocalStorage = exports.asyncLocalStorage = void 0;
|
|
4
4
|
const async_hooks_1 = require("async_hooks");
|
|
5
5
|
exports.asyncLocalStorage = new async_hooks_1.AsyncLocalStorage();
|
|
6
6
|
exports.activityAsyncLocalStorage = new async_hooks_1.AsyncLocalStorage();
|
|
7
|
+
exports.virtualAsyncLocalStorage = new async_hooks_1.AsyncLocalStorage();
|
package/build/package.json
CHANGED
|
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.processEvent = void 0;
|
|
4
4
|
const enums_1 = require("../../../modules/enums");
|
|
5
5
|
const errors_1 = require("../../../modules/errors");
|
|
6
|
-
const collator_1 = require("
|
|
6
|
+
const collator_1 = require("../../../types/collator");
|
|
7
|
+
const collator_2 = require("../../collator");
|
|
7
8
|
const telemetry_1 = require("../../telemetry");
|
|
8
9
|
const stream_1 = require("../../../types/stream");
|
|
9
10
|
// Per-instance collation error tracking for reservation timeout detection
|
|
@@ -34,7 +35,7 @@ async function processEvent(instance, status = stream_1.StreamStatus.SUCCESS, co
|
|
|
34
35
|
try {
|
|
35
36
|
const collationKey = await instance.verifyReentry();
|
|
36
37
|
instance.adjacentIndex =
|
|
37
|
-
|
|
38
|
+
collator_2.CollatorService.getDimensionalIndex(collationKey);
|
|
38
39
|
telemetry = new telemetry_1.TelemetryService(instance.engine.appId, instance.config, instance.metadata, instance.context);
|
|
39
40
|
telemetry.startActivitySpan(instance.leg);
|
|
40
41
|
//bind data per status type
|
|
@@ -71,10 +72,27 @@ async function processEvent(instance, status = stream_1.StreamStatus.SUCCESS, co
|
|
|
71
72
|
}
|
|
72
73
|
catch (error) {
|
|
73
74
|
if (error instanceof errors_1.CollationError) {
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
75
|
+
//FORBIDDEN: Leg1 not complete — signal arrived in the window
|
|
76
|
+
//between registerHook (standalone) and Leg1 transaction commit.
|
|
77
|
+
//Rethrow so the stream message is retried with backoff; by then
|
|
78
|
+
//Leg1 will have committed and Leg2 processing will succeed.
|
|
79
|
+
//The GUID marker was already committed by notarizeLeg2Entry;
|
|
80
|
+
//on retry, collateLeg2Entry's SETNX is a no-op for the same
|
|
81
|
+
//GUID, and verifySyntheticInteger sees no steps done → allowed.
|
|
82
|
+
if (error.fault === collator_1.CollationFaultType.FORBIDDEN) {
|
|
83
|
+
instance.logger.warn('process-event-forbidden-retry', {
|
|
84
|
+
jid: instance.context.metadata.jid,
|
|
85
|
+
aid: instance.metadata.aid,
|
|
86
|
+
message: 'Leg1 not committed yet; rethrowing for stream retry',
|
|
87
|
+
error,
|
|
88
|
+
});
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
// INACTIVE/DUPLICATE: legitimate duplicate detection — the
|
|
92
|
+
// Postgres atomic CTE (collateLeg2Entry) serializes via row
|
|
93
|
+
// locks, so the GUID ledger value is correct. Silent ack is
|
|
94
|
+
// the right behavior: the work was already done by a prior
|
|
95
|
+
// delivery of this message.
|
|
78
96
|
const now = Date.now();
|
|
79
97
|
if (now - collationWindowStart > COLLATION_WINDOW_MS) {
|
|
80
98
|
collationErrorCount = 0;
|
|
@@ -30,9 +30,12 @@ class CronHandler {
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
nextDelay(cronExpression) {
|
|
33
|
-
if (
|
|
33
|
+
if (cronExpression == null || typeof cronExpression !== 'string') {
|
|
34
34
|
return -1;
|
|
35
35
|
}
|
|
36
|
+
if (!(0, utils_1.isValidCron)(cronExpression)) {
|
|
37
|
+
throw new Error(`Invalid cron expression: ${cronExpression}`);
|
|
38
|
+
}
|
|
36
39
|
const interval = (0, cron_parser_1.parseExpression)(cronExpression, { utc: true });
|
|
37
40
|
const nextDate = interval.next().toDate();
|
|
38
41
|
const now = new Date();
|
|
@@ -118,9 +118,14 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
|
|
|
118
118
|
/**
|
|
119
119
|
* Leg1: set hook signal, atomically detecting a pending signal.
|
|
120
120
|
*
|
|
121
|
-
* Standalone (no transaction):
|
|
122
|
-
*
|
|
123
|
-
*
|
|
121
|
+
* Standalone (no transaction): acquires a per-key advisory lock to
|
|
122
|
+
* serialize with concurrent getHookSignal calls, then reads any
|
|
123
|
+
* existing pending value and inserts the hook signal.
|
|
124
|
+
*
|
|
125
|
+
* The advisory lock prevents a race where the CTE's read snapshot
|
|
126
|
+
* misses a concurrently inserted pending signal — under READ
|
|
127
|
+
* COMMITTED, ON CONFLICT sees committed writes but the SELECT CTE
|
|
128
|
+
* does not, causing the pending data to be silently overwritten.
|
|
124
129
|
*
|
|
125
130
|
* In a transaction: queues the setnxex; pending detection deferred.
|
|
126
131
|
*/
|
|
@@ -132,10 +137,13 @@ declare class PostgresStoreService extends StoreService<ProviderClient, Provider
|
|
|
132
137
|
* Leg2: get hook signal OR atomically set a pending signal.
|
|
133
138
|
*
|
|
134
139
|
* When `pendingData` is provided and no hook signal exists, the
|
|
135
|
-
* pending value is
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
140
|
+
* pending value is stored so leg1's setHookSignal can detect it.
|
|
141
|
+
*
|
|
142
|
+
* Uses a per-key advisory lock to serialize with concurrent
|
|
143
|
+
* setHookSignal calls. Without the lock, a CTE race exists where
|
|
144
|
+
* the read snapshot misses a concurrently inserted hook signal AND
|
|
145
|
+
* the pending INSERT fails on conflict (the hook has valid expiry),
|
|
146
|
+
* silently losing the signal.
|
|
139
147
|
*
|
|
140
148
|
* When `pendingData` is omitted, behaves as a plain read.
|
|
141
149
|
*/
|
|
@@ -755,9 +755,14 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
755
755
|
/**
|
|
756
756
|
* Leg1: set hook signal, atomically detecting a pending signal.
|
|
757
757
|
*
|
|
758
|
-
* Standalone (no transaction):
|
|
759
|
-
*
|
|
760
|
-
*
|
|
758
|
+
* Standalone (no transaction): acquires a per-key advisory lock to
|
|
759
|
+
* serialize with concurrent getHookSignal calls, then reads any
|
|
760
|
+
* existing pending value and inserts the hook signal.
|
|
761
|
+
*
|
|
762
|
+
* The advisory lock prevents a race where the CTE's read snapshot
|
|
763
|
+
* misses a concurrently inserted pending signal — under READ
|
|
764
|
+
* COMMITTED, ON CONFLICT sees committed writes but the SELECT CTE
|
|
765
|
+
* does not, causing the pending data to be silently overwritten.
|
|
761
766
|
*
|
|
762
767
|
* In a transaction: queues the setnxex; pending detection deferred.
|
|
763
768
|
*/
|
|
@@ -774,37 +779,30 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
774
779
|
const kv = this.kvsql();
|
|
775
780
|
const tableName = kv.tableForKey(fullKey);
|
|
776
781
|
const storedKey = kv.storageKey(fullKey);
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
SELECT
|
|
780
|
-
WHERE key = $1 AND (expiry IS NULL OR expiry > NOW())
|
|
781
|
-
),
|
|
782
|
-
ins AS (
|
|
783
|
-
INSERT INTO ${tableName} (key, value, expiry)
|
|
784
|
-
VALUES ($1, $2, NOW() + INTERVAL '${delay} seconds')
|
|
785
|
-
ON CONFLICT (key) DO UPDATE
|
|
786
|
-
SET value = EXCLUDED.value, expiry = EXCLUDED.expiry
|
|
787
|
-
WHERE ${tableName}.expiry IS NULL
|
|
788
|
-
OR ${tableName}.expiry <= NOW()
|
|
789
|
-
OR ${tableName}.value LIKE '$pending::%'
|
|
790
|
-
RETURNING true as success
|
|
791
|
-
)
|
|
792
|
-
SELECT
|
|
793
|
-
COALESCE((SELECT success FROM ins), false) as success,
|
|
794
|
-
(SELECT value FROM pre) as existing_value
|
|
795
|
-
`;
|
|
782
|
+
//acquire per-key advisory lock (session-level) to serialize
|
|
783
|
+
//with concurrent getHookSignal for the same signal key
|
|
784
|
+
await this.pgClient.query('SELECT pg_advisory_lock(901, hashtext($1))', [storedKey]);
|
|
796
785
|
try {
|
|
797
|
-
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
if (
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
pendingData
|
|
805
|
-
}
|
|
786
|
+
//read existing value under lock
|
|
787
|
+
const readRes = await this.pgClient.query(`SELECT value FROM ${tableName}
|
|
788
|
+
WHERE key = $1 AND (expiry IS NULL OR expiry > NOW())`, [storedKey]);
|
|
789
|
+
let pendingData;
|
|
790
|
+
if (readRes.rows.length > 0) {
|
|
791
|
+
const existing = readRes.rows[0].value;
|
|
792
|
+
if (existing?.startsWith('$pending::')) {
|
|
793
|
+
pendingData = existing.slice('$pending::'.length);
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
//hook already set (retry) — no change needed
|
|
797
|
+
return { success: false };
|
|
798
|
+
}
|
|
806
799
|
}
|
|
807
|
-
|
|
800
|
+
//insert hook value (or overwrite pending)
|
|
801
|
+
await this.pgClient.query(`INSERT INTO ${tableName} (key, value, expiry)
|
|
802
|
+
VALUES ($1, $2, NOW() + INTERVAL '${delay} seconds')
|
|
803
|
+
ON CONFLICT (key) DO UPDATE
|
|
804
|
+
SET value = EXCLUDED.value, expiry = EXCLUDED.expiry`, [storedKey, jobId]);
|
|
805
|
+
return { success: true, pendingData };
|
|
808
806
|
}
|
|
809
807
|
catch (error) {
|
|
810
808
|
if (error?.message?.includes('closed') ||
|
|
@@ -813,15 +811,26 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
813
811
|
}
|
|
814
812
|
throw error;
|
|
815
813
|
}
|
|
814
|
+
finally {
|
|
815
|
+
try {
|
|
816
|
+
await this.pgClient.query('SELECT pg_advisory_unlock(901, hashtext($1))', [storedKey]);
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
//lock auto-releases on session close
|
|
820
|
+
}
|
|
821
|
+
}
|
|
816
822
|
}
|
|
817
823
|
/**
|
|
818
824
|
* Leg2: get hook signal OR atomically set a pending signal.
|
|
819
825
|
*
|
|
820
826
|
* When `pendingData` is provided and no hook signal exists, the
|
|
821
|
-
* pending value is
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
827
|
+
* pending value is stored so leg1's setHookSignal can detect it.
|
|
828
|
+
*
|
|
829
|
+
* Uses a per-key advisory lock to serialize with concurrent
|
|
830
|
+
* setHookSignal calls. Without the lock, a CTE race exists where
|
|
831
|
+
* the read snapshot misses a concurrently inserted hook signal AND
|
|
832
|
+
* the pending INSERT fails on conflict (the hook has valid expiry),
|
|
833
|
+
* silently losing the signal.
|
|
825
834
|
*
|
|
826
835
|
* When `pendingData` is omitted, behaves as a plain read.
|
|
827
836
|
*/
|
|
@@ -838,38 +847,30 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
838
847
|
return undefined;
|
|
839
848
|
return value;
|
|
840
849
|
}
|
|
841
|
-
//atomic get-or-set-pending: one round trip
|
|
842
850
|
const kv = this.kvsql();
|
|
843
851
|
const tableName = kv.tableForKey(fullKey);
|
|
844
852
|
const storedKey = kv.storageKey(fullKey);
|
|
845
853
|
const expire = pendingExpire || enums_1.HMSH_PENDING_SIGNAL_EXPIRE;
|
|
846
854
|
const pendingValue = `$pending::${pendingData}`;
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
SELECT
|
|
850
|
-
WHERE key = $1 AND (expiry IS NULL OR expiry > NOW())
|
|
851
|
-
),
|
|
852
|
-
pending AS (
|
|
853
|
-
INSERT INTO ${tableName} (key, value, expiry)
|
|
854
|
-
SELECT $1, $2, NOW() + INTERVAL '${expire} seconds'
|
|
855
|
-
WHERE NOT EXISTS (SELECT 1 FROM existing)
|
|
856
|
-
ON CONFLICT (key) DO UPDATE
|
|
857
|
-
SET value = EXCLUDED.value, expiry = EXCLUDED.expiry
|
|
858
|
-
WHERE ${tableName}.expiry IS NULL OR ${tableName}.expiry <= NOW()
|
|
859
|
-
RETURNING true as inserted
|
|
860
|
-
)
|
|
861
|
-
SELECT
|
|
862
|
-
(SELECT value FROM existing) as hook_value,
|
|
863
|
-
(SELECT inserted FROM pending) as pending_inserted
|
|
864
|
-
`;
|
|
855
|
+
//acquire per-key advisory lock (session-level) to serialize
|
|
856
|
+
//with concurrent setHookSignal for the same signal key
|
|
857
|
+
await this.pgClient.query('SELECT pg_advisory_lock(901, hashtext($1))', [storedKey]);
|
|
865
858
|
try {
|
|
866
|
-
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
if (
|
|
870
|
-
|
|
859
|
+
//read existing value under lock
|
|
860
|
+
const readRes = await this.pgClient.query(`SELECT value FROM ${tableName}
|
|
861
|
+
WHERE key = $1 AND (expiry IS NULL OR expiry > NOW())`, [storedKey]);
|
|
862
|
+
if (readRes.rows.length > 0) {
|
|
863
|
+
const value = readRes.rows[0].value;
|
|
864
|
+
if (value && !value.startsWith('$pending::')) {
|
|
865
|
+
//hook found — return it
|
|
866
|
+
return value;
|
|
867
|
+
}
|
|
871
868
|
}
|
|
872
|
-
//no hook signal
|
|
869
|
+
//no hook signal — store pending
|
|
870
|
+
await this.pgClient.query(`INSERT INTO ${tableName} (key, value, expiry)
|
|
871
|
+
VALUES ($1, $2, NOW() + INTERVAL '${expire} seconds')
|
|
872
|
+
ON CONFLICT (key) DO UPDATE
|
|
873
|
+
SET value = EXCLUDED.value, expiry = EXCLUDED.expiry`, [storedKey, pendingValue]);
|
|
873
874
|
return undefined;
|
|
874
875
|
}
|
|
875
876
|
catch (error) {
|
|
@@ -879,6 +880,14 @@ class PostgresStoreService extends __1.StoreService {
|
|
|
879
880
|
}
|
|
880
881
|
throw error;
|
|
881
882
|
}
|
|
883
|
+
finally {
|
|
884
|
+
try {
|
|
885
|
+
await this.pgClient.query('SELECT pg_advisory_unlock(901, hashtext($1))', [storedKey]);
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
//lock auto-releases on session close
|
|
889
|
+
}
|
|
890
|
+
}
|
|
882
891
|
}
|
|
883
892
|
async deleteHookSignal(topic, resolved) {
|
|
884
893
|
const key = this.mintKey(key_1.KeyType.SIGNALS, { appId: this.appId });
|
|
@@ -42,7 +42,7 @@ class TaskService {
|
|
|
42
42
|
async registerTimeHook(jobId, gId, activityId, type, inSeconds = enums_1.HMSH_FIDELITY_SECONDS, dad, transaction) {
|
|
43
43
|
const fromNow = Date.now() + inSeconds * 1000;
|
|
44
44
|
const fidelityMS = enums_1.HMSH_FIDELITY_SECONDS * 1000;
|
|
45
|
-
const awakenTimeSlot = Math.
|
|
45
|
+
const awakenTimeSlot = Math.ceil(fromNow / fidelityMS) * fidelityMS;
|
|
46
46
|
await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, dad, transaction);
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HotMesh } from '../hotmesh';
|
|
2
|
-
import { VirtualConnectParams, VirtualCronParams, VirtualExecParams, VirtualFlushParams, VirtualInstanceOptions, VirtualInterruptParams } from '../../types/virtual';
|
|
2
|
+
import { VirtualConnectParams, VirtualContext, VirtualCronParams, VirtualExecParams, VirtualFlushParams, VirtualInstanceOptions, VirtualInterruptParams } from '../../types/virtual';
|
|
3
3
|
import { ProviderConfig, ProvidersConfig } from '../../types/provider';
|
|
4
4
|
/**
|
|
5
5
|
* Virtual creates a virtual network of functions, connecting any
|
|
@@ -185,6 +185,27 @@ declare class Virtual {
|
|
|
185
185
|
* ```
|
|
186
186
|
*/
|
|
187
187
|
static interrupt(params: VirtualInterruptParams): Promise<boolean>;
|
|
188
|
+
/**
|
|
189
|
+
* Returns the execution context for the current Virtual callback.
|
|
190
|
+
* Must be called from inside a `Virtual.cron` or `Virtual.exec`
|
|
191
|
+
* callback — throws if called outside that scope.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```typescript
|
|
195
|
+
* await Virtual.cron({
|
|
196
|
+
* topic: 'my.cron',
|
|
197
|
+
* connection,
|
|
198
|
+
* args: [],
|
|
199
|
+
* options: { id: 'daily', interval: '0 0 * * *' },
|
|
200
|
+
* callback: async () => {
|
|
201
|
+
* const ctx = Virtual.getContext();
|
|
202
|
+
* console.log(ctx.workflowId); // 'daily'
|
|
203
|
+
* console.log(ctx.attempt); // 1
|
|
204
|
+
* },
|
|
205
|
+
* });
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
static getContext(): VirtualContext;
|
|
188
209
|
/**
|
|
189
210
|
* Shuts down all virtual instances. Call this method
|
|
190
211
|
* from the SIGTERM handler in your application.
|
|
@@ -6,8 +6,14 @@ const hotmesh_1 = require("../hotmesh");
|
|
|
6
6
|
const enums_1 = require("../../modules/enums");
|
|
7
7
|
const utils_1 = require("../../modules/utils");
|
|
8
8
|
const key_1 = require("../../modules/key");
|
|
9
|
+
const storage_1 = require("../../modules/storage");
|
|
9
10
|
const cron_1 = require("../pipe/functions/cron");
|
|
10
11
|
const factory_1 = require("./schemas/factory");
|
|
12
|
+
const DEFAULT_RETRY_POLICY = {
|
|
13
|
+
maximumAttempts: 3,
|
|
14
|
+
backoffCoefficient: 2,
|
|
15
|
+
maximumInterval: 30,
|
|
16
|
+
};
|
|
11
17
|
/**
|
|
12
18
|
* Virtual creates a virtual network of functions, connecting any
|
|
13
19
|
* function as an idempotent, cacheable endpoint. Call functions
|
|
@@ -173,13 +179,19 @@ class Virtual {
|
|
|
173
179
|
{
|
|
174
180
|
topic: params.topic,
|
|
175
181
|
connection,
|
|
176
|
-
retry: params.retry ??
|
|
177
|
-
maximumAttempts: 3,
|
|
178
|
-
backoffCoefficient: 2,
|
|
179
|
-
maximumInterval: 30,
|
|
180
|
-
},
|
|
182
|
+
retry: params.retry ?? DEFAULT_RETRY_POLICY,
|
|
181
183
|
callback: async function (input) {
|
|
182
|
-
const
|
|
184
|
+
const context = new Map([
|
|
185
|
+
['topic', params.topic],
|
|
186
|
+
['workflowId', input.metadata.jid ?? ''],
|
|
187
|
+
['workflowName', input.metadata.wfn ?? ''],
|
|
188
|
+
['dimension', input.metadata.dad ?? ''],
|
|
189
|
+
['attempt', input.metadata.try ?? 1],
|
|
190
|
+
['guid', input.metadata.guid],
|
|
191
|
+
['traceId', input.metadata.trc ?? ''],
|
|
192
|
+
['spanId', input.metadata.spn ?? ''],
|
|
193
|
+
]);
|
|
194
|
+
const response = await storage_1.virtualAsyncLocalStorage.run(context, () => params.callback.apply(this, input.data.args));
|
|
183
195
|
return {
|
|
184
196
|
metadata: { ...input.metadata },
|
|
185
197
|
data: { response },
|
|
@@ -386,6 +398,42 @@ class Virtual {
|
|
|
386
398
|
}
|
|
387
399
|
return true;
|
|
388
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Returns the execution context for the current Virtual callback.
|
|
403
|
+
* Must be called from inside a `Virtual.cron` or `Virtual.exec`
|
|
404
|
+
* callback — throws if called outside that scope.
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* ```typescript
|
|
408
|
+
* await Virtual.cron({
|
|
409
|
+
* topic: 'my.cron',
|
|
410
|
+
* connection,
|
|
411
|
+
* args: [],
|
|
412
|
+
* options: { id: 'daily', interval: '0 0 * * *' },
|
|
413
|
+
* callback: async () => {
|
|
414
|
+
* const ctx = Virtual.getContext();
|
|
415
|
+
* console.log(ctx.workflowId); // 'daily'
|
|
416
|
+
* console.log(ctx.attempt); // 1
|
|
417
|
+
* },
|
|
418
|
+
* });
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
421
|
+
static getContext() {
|
|
422
|
+
const store = storage_1.virtualAsyncLocalStorage.getStore();
|
|
423
|
+
if (!store) {
|
|
424
|
+
throw new Error('Virtual.getContext() called outside of a Virtual callback execution context');
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
topic: store.get('topic'),
|
|
428
|
+
workflowId: store.get('workflowId'),
|
|
429
|
+
workflowName: store.get('workflowName'),
|
|
430
|
+
dimension: store.get('dimension'),
|
|
431
|
+
attempt: store.get('attempt'),
|
|
432
|
+
guid: store.get('guid'),
|
|
433
|
+
traceId: store.get('traceId'),
|
|
434
|
+
spanId: store.get('spanId'),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
389
437
|
/**
|
|
390
438
|
* Shuts down all virtual instances. Call this method
|
|
391
439
|
* from the SIGTERM handler in your application.
|
package/build/types/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { ExtensionType, JobCompletionOptions, JobData, JobsData, JobInterruptOpt
|
|
|
15
15
|
export { MappingStatements } from './map';
|
|
16
16
|
export { Pipe, PipeContext, PipeItem, PipeItems, PipeObject, ReduceObject, } from './pipe';
|
|
17
17
|
export { ProviderClass, ProviderClient, ProviderConfig, ProviderTransaction, Providers, TransactionResultList, ProviderNativeClient, ProviderOptions, } from './provider';
|
|
18
|
-
export { VirtualConnectParams, VirtualExecParams, VirtualCronParams, VirtualExecOptions, VirtualCronOptions, VirtualInterruptOptions, VirtualInterruptParams, VirtualFlushParams, } from './virtual';
|
|
18
|
+
export { VirtualConnectParams, VirtualContext, VirtualExecParams, VirtualCronParams, VirtualExecOptions, VirtualCronOptions, VirtualInterruptOptions, VirtualInterruptParams, VirtualFlushParams, } from './virtual';
|
|
19
19
|
export { PostgresClassType, PostgresClientOptions, PostgresClientType, PostgresConsumerGroup, PostgresPendingMessage, PostgresPoolClientType, PostgresQueryConfigType, PostgresQueryResultType, PostgresStreamMessage, PostgresStreamOptions, PostgresTransaction, } from './postgres';
|
|
20
20
|
export { ActivateMessage, CronMessage, JobMessage, JobMessageCallback, PingMessage, PongMessage, QuorumMessage, QuorumMessageCallback, QuorumProfile, RollCallMessage, RollCallOptions, SubscriptionCallback, SubscriptionOptions, SystemHealth, ThrottleMessage, ThrottleOptions, WorkMessage, } from './quorum';
|
|
21
21
|
export { NatsAckPolicy, NatsAckPolicyExplicitType, NatsClassType, NatsClientType, NatsClientOptions, NatsConsumerConfigType, NatsJetStreamManager, NatsConnection, NatsJetStreamType, NatsConnectionOptions, NatsConsumerConfig, NatsConsumerInfo, NatsConsumerManager, NatsDeliveryInfo, NatsJetStreamOptions, NatsError, NatsErrorType, NatsJetStreamClient, NatsJsMsg, NatsMessageType, NatsMsgExpect, NatsPubAck, NatsPubAckType, NatsPublishOptions, NatsRetentionPolicy, NatsRetentionPolicyWorkqueueType, NatsSequenceInfo, NatsStorageMemoryType, NatsStorageType, NatsStreamConfig, NatsStreamInfo, NatsStreamManager, NatsStreamConfigType, NatsStreamInfoType, NatsStreamOptions, NatsStreamState, NatsTransaction, } from './nats';
|
package/build/types/virtual.d.ts
CHANGED
|
@@ -208,4 +208,72 @@ interface VirtualInterruptParams {
|
|
|
208
208
|
*/
|
|
209
209
|
options: VirtualInterruptOptions;
|
|
210
210
|
}
|
|
211
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Execution context available inside Virtual callbacks via
|
|
213
|
+
* `Virtual.getContext()`. Populated automatically by the
|
|
214
|
+
* AsyncLocalStorage wrapper in `Virtual.connect()`.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```typescript
|
|
218
|
+
* await Virtual.cron({
|
|
219
|
+
* topic: 'billing.daily',
|
|
220
|
+
* connection,
|
|
221
|
+
* args: [],
|
|
222
|
+
* options: { id: 'billing-run', interval: '0 0 * * *' },
|
|
223
|
+
* callback: async () => {
|
|
224
|
+
* const ctx = Virtual.getContext();
|
|
225
|
+
* // ctx.workflowId === 'billing-run'
|
|
226
|
+
* // ctx.topic === 'billing.daily'
|
|
227
|
+
* // ctx.guid === unique per invocation
|
|
228
|
+
* },
|
|
229
|
+
* });
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
interface VirtualContext {
|
|
233
|
+
/**
|
|
234
|
+
* The worker topic that routed this invocation.
|
|
235
|
+
* Matches the `topic` passed to `Virtual.cron()` or
|
|
236
|
+
* `Virtual.connect()`.
|
|
237
|
+
*/
|
|
238
|
+
topic: string;
|
|
239
|
+
/**
|
|
240
|
+
* Workflow / job ID. For cron callbacks this is the `id`
|
|
241
|
+
* from `options.id`. For `Virtual.exec` calls this is the
|
|
242
|
+
* auto-generated or user-supplied job identifier.
|
|
243
|
+
*/
|
|
244
|
+
workflowId: string;
|
|
245
|
+
/**
|
|
246
|
+
* Internal workflow name (the graph subscription topic,
|
|
247
|
+
* e.g. `hmsh.cron` or `hmsh.call`).
|
|
248
|
+
*/
|
|
249
|
+
workflowName: string;
|
|
250
|
+
/**
|
|
251
|
+
* Dimensional address for this execution within the
|
|
252
|
+
* workflow's DAG. Encodes the cycle iteration and
|
|
253
|
+
* parallel branch position.
|
|
254
|
+
*/
|
|
255
|
+
dimension: string;
|
|
256
|
+
/**
|
|
257
|
+
* Current retry attempt (1-based). `1` on the first try,
|
|
258
|
+
* incremented on each retry per the `RetryPolicy`.
|
|
259
|
+
*/
|
|
260
|
+
attempt: number;
|
|
261
|
+
/**
|
|
262
|
+
* Globally unique identifier for the stream message that
|
|
263
|
+
* triggered this callback. A new GUID is minted for every
|
|
264
|
+
* invocation, including retries and cycle iterations, so it
|
|
265
|
+
* serves as an idempotency key for exactly-once processing.
|
|
266
|
+
*/
|
|
267
|
+
guid: string;
|
|
268
|
+
/**
|
|
269
|
+
* OpenTelemetry trace ID propagated from the originating
|
|
270
|
+
* workflow. Empty string when tracing is not configured.
|
|
271
|
+
*/
|
|
272
|
+
traceId: string;
|
|
273
|
+
/**
|
|
274
|
+
* OpenTelemetry span ID propagated from the parent activity.
|
|
275
|
+
* Empty string when tracing is not configured.
|
|
276
|
+
*/
|
|
277
|
+
spanId: string;
|
|
278
|
+
}
|
|
279
|
+
export { VirtualConnectParams, VirtualContext, VirtualExecParams, VirtualCronParams, VirtualExecOptions, VirtualCronOptions, VirtualInterruptOptions, VirtualInterruptParams, VirtualFlushOptions, VirtualFlushParams, VirtualInstanceOptions, };
|