@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.
@@ -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>>;
@@ -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();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -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("../../collator");
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
- collator_1.CollatorService.getDimensionalIndex(collationKey);
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
- // INACTIVE is legitimate duplicate detection the Postgres atomic
75
- // CTE (collateLeg2Entry) serializes via row locks, so the GUID
76
- // ledger value is correct. Silent ack is the right behavior:
77
- // the work was already done by a prior delivery of this message.
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 (!(0, utils_1.isValidCron)(cronExpression)) {
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): single CTE query that reads any existing
122
- * pending value, then inserts the hook signal (overwriting pending or
123
- * expired entries). Returns `{success, pendingData}` in one round trip.
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 inserted in the SAME SQL statement — no second
136
- * round trip. This is the transactional edge that prevents the
137
- * signal from being lost: by the time the query returns, the
138
- * pending key is already visible to leg1's setnxex.
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): single CTE query that reads any existing
759
- * pending value, then inserts the hook signal (overwriting pending or
760
- * expired entries). Returns `{success, pendingData}` in one round trip.
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
- const sql = `
778
- WITH pre AS (
779
- SELECT value FROM ${tableName}
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
- const res = await this.pgClient.query(sql, [storedKey, jobId]);
798
- const row = res.rows[0] || {};
799
- const success = row.success === true;
800
- const existing = row.existing_value;
801
- if (success && existing?.startsWith('$pending::')) {
802
- return {
803
- success: true,
804
- pendingData: existing.slice('$pending::'.length),
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
- return { success };
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 inserted in the SAME SQL statement — no second
822
- * round trip. This is the transactional edge that prevents the
823
- * signal from being lost: by the time the query returns, the
824
- * pending key is already visible to leg1's setnxex.
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
- const sql = `
848
- WITH existing AS (
849
- SELECT value FROM ${tableName}
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
- const res = await this.pgClient.query(sql, [storedKey, pendingValue]);
867
- const row = res.rows[0] || {};
868
- const hookValue = row.hook_value;
869
- if (hookValue && !hookValue.startsWith('$pending::')) {
870
- return hookValue;
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; pending was inserted (or already existed)
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.floor(fromNow / fidelityMS) * fidelityMS;
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 response = await params.callback.apply(this, input.data.args);
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.
@@ -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';
@@ -208,4 +208,72 @@ interface VirtualInterruptParams {
208
208
  */
209
209
  options: VirtualInterruptOptions;
210
210
  }
211
- export { VirtualConnectParams, VirtualExecParams, VirtualCronParams, VirtualExecOptions, VirtualCronOptions, VirtualInterruptOptions, VirtualInterruptParams, VirtualFlushOptions, VirtualFlushParams, VirtualInstanceOptions, };
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, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",