@hotmeshio/hotmesh 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +12 -129
  2. package/build/modules/utils.js +3 -0
  3. package/build/package.json +2 -1
  4. package/build/services/activities/hook.d.ts +178 -58
  5. package/build/services/activities/hook.js +244 -58
  6. package/build/services/activities/trigger.js +5 -1
  7. package/build/services/durable/client.d.ts +238 -66
  8. package/build/services/durable/client.js +309 -125
  9. package/build/services/durable/index.d.ts +0 -2
  10. package/build/services/durable/schemas/factory.js +40 -0
  11. package/build/services/durable/worker.js +5 -28
  12. package/build/services/durable/workflow/condition.d.ts +69 -37
  13. package/build/services/durable/workflow/condition.js +70 -39
  14. package/build/services/hotmesh/index.d.ts +31 -4
  15. package/build/services/hotmesh/index.js +31 -4
  16. package/build/services/store/index.d.ts +1 -1
  17. package/build/services/store/providers/postgres/kvsql.d.ts +1 -1
  18. package/build/services/store/providers/postgres/kvtables.js +83 -122
  19. package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +1 -1
  20. package/build/services/store/providers/postgres/kvtypes/hash/basic.js +8 -8
  21. package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +1 -1
  22. package/build/services/store/providers/postgres/postgres.d.ts +44 -157
  23. package/build/services/store/providers/postgres/postgres.js +480 -278
  24. package/build/types/activity.d.ts +2 -0
  25. package/build/types/hmsh_escalations.d.ts +212 -0
  26. package/build/types/index.d.ts +1 -1
  27. package/build/types/provider.d.ts +2 -0
  28. package/package.json +2 -1
  29. package/build/types/signal.d.ts +0 -147
  30. /package/build/types/{signal.js → hmsh_escalations.js} +0 -0
@@ -285,154 +285,338 @@ class ClientService {
285
285
  },
286
286
  };
287
287
  /**
288
- * Signal queue API for managing paused-workflow task records.
289
- * Operations: list, get, claim, claimByMetadata, release, resolve,
290
- * resolveByMetadata, releaseExpired.
288
+ * Escalation queue operations over `public.hmsh_escalations` a global
289
+ * table that surfaces workflow signal pauses as role-based, claimable,
290
+ * searchable queue items.
291
291
  *
292
- * Requires a Postgres store provider. Methods are no-ops (return null/false)
293
- * when called against a non-Postgres store.
292
+ * When a YAML `hook` activity suspends with an `escalation:` block, or
293
+ * `Durable.workflow.condition(signalId, config)` fires, **one row is
294
+ * written atomically** with the workflow checkpoint — no enrichment step,
295
+ * no secondary round-trip. Every connected app shares the same table;
296
+ * rows are namespaced by `namespace` + `app_id`.
294
297
  *
295
- * @example
298
+ * **Status lifecycle:**
299
+ * ```
300
+ * pending → claimed → resolved
301
+ * ↘ cancelled (any non-terminal state)
302
+ * ↗ pending (via release or releaseExpired)
303
+ * ```
304
+ *
305
+ * **Typical human-in-the-loop flow:**
296
306
  * ```typescript
297
- * // Claim a pending task by metadata key
298
- * const task = await client.signalQueue.claimByMetadata({
299
- * key: 'orderId', value: 'RX-123',
300
- * assignee: 'pharmacist-jane',
301
- * durationMinutes: 30,
307
+ * // 1. Workflow pauses and writes the escalation row automatically
308
+ * const decision = await Durable.workflow.condition('manager-approval', {
309
+ * role: 'manager',
310
+ * type: 'order-approval',
311
+ * priority: 2,
312
+ * metadata: { orderId },
302
313
  * });
303
314
  *
304
- * if (task) {
305
- * await client.signalQueue.resolve({
306
- * id: task.id,
307
- * resolverPayload: { approved: true },
308
- * });
309
- * // → paused workflow resumes with { approved: true }
310
- * }
315
+ * // 2. Dashboard lists pending approvals for this role
316
+ * const [item] = await client.escalations.list({ role: 'manager', status: 'pending' });
317
+ *
318
+ * // 3. Reviewer claims it (sets assigned_to + expiry)
319
+ * await client.escalations.claim({ id: item.id, assignee: 'alice@company.com' });
320
+ *
321
+ * // 4. Resolve atomically marks it resolved AND delivers the signal
322
+ * await client.escalations.resolve({
323
+ * id: item.id,
324
+ * resolverPayload: { approved: true },
325
+ * });
326
+ * // workflow resumes with { approved: true }
311
327
  * ```
312
328
  */
313
- this.signalQueue = {
314
- list: async (params = {}) => {
315
- const hotMesh = await this.getHotMeshClient(null, params.namespace);
316
- const store = hotMesh.engine.store;
317
- if (typeof store.listSignals !== 'function')
318
- return [];
319
- const ns = params.namespace ?? factory_1.APP_ID;
320
- return store.listSignals({
321
- namespace: ns,
322
- appId: store.appId,
323
- status: params.status,
324
- role: params.role,
325
- taskQueue: params.taskQueue,
326
- limit: params.limit,
327
- offset: params.offset,
328
- });
329
+ this.escalations = {
330
+ /**
331
+ * Returns all escalation rows matching the given filters.
332
+ *
333
+ * @example
334
+ * ```typescript
335
+ * // All pending approvals for the manager role
336
+ * const items = await client.escalations.list({ role: 'manager', status: 'pending' });
337
+ *
338
+ * // By workflow ID
339
+ * const items = await client.escalations.list({ workflowId: 'order-123' });
340
+ * ```
341
+ */
342
+ list: async (params) => {
343
+ const hotMeshClient = await this.getHotMeshClient(null, params?.namespace);
344
+ return hotMeshClient.engine.store.listEscalations(params ?? {});
329
345
  },
346
+ /**
347
+ * Returns a single escalation row by its UUID primary key.
348
+ * Returns `null` if not found.
349
+ */
330
350
  get: async (id, namespace) => {
331
- const hotMesh = await this.getHotMeshClient(null, namespace);
332
- const store = hotMesh.engine.store;
333
- if (typeof store.getSignal !== 'function')
334
- return null;
335
- const ns = namespace ?? factory_1.APP_ID;
336
- return store.getSignal({ namespace: ns, appId: store.appId, id });
351
+ const hotMeshClient = await this.getHotMeshClient(null, namespace);
352
+ return hotMeshClient.engine.store.getEscalation(id, namespace);
353
+ },
354
+ /**
355
+ * Looks up an escalation row by its `signal_key` — the value that was
356
+ * passed to `condition()` or stored in the hook activity's collation rule.
357
+ * This is the same key used to deliver the signal via `hotMesh.signal()`.
358
+ *
359
+ * @example
360
+ * ```typescript
361
+ * const item = await client.escalations.getBySignalKey('manager-approval');
362
+ * ```
363
+ */
364
+ getBySignalKey: async (signalKey, namespace) => {
365
+ const hotMeshClient = await this.getHotMeshClient(null, namespace);
366
+ return hotMeshClient.engine.store.getEscalationBySignalKey(signalKey, namespace);
367
+ },
368
+ /**
369
+ * Creates a standalone escalation row that is **not** backed by a signal.
370
+ * `signal_key` is `null`. Useful for external task tracking that doesn't
371
+ * need to resume a workflow (e.g., audit tasks, out-of-band approvals).
372
+ *
373
+ * @example
374
+ * ```typescript
375
+ * const entry = await client.escalations.create({
376
+ * role: 'support',
377
+ * type: 'data-correction',
378
+ * description: 'Fix the customer address',
379
+ * metadata: { customerId: 'cust-42' },
380
+ * });
381
+ * ```
382
+ */
383
+ create: async (params) => {
384
+ const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
385
+ return hotMeshClient.engine.store.createEscalation(params);
386
+ },
387
+ /**
388
+ * Patches an existing escalation row. All fields are optional — only
389
+ * provided fields are written. `metadata` is **merged**, not replaced.
390
+ *
391
+ * Signal routing fields (`signalKey`, `topic`, `workflowId`, …) can be
392
+ * enriched after the row is created — useful when the row is created
393
+ * before the workflow starts and routing context is not yet known.
394
+ *
395
+ * @example
396
+ * ```typescript
397
+ * await client.escalations.update({
398
+ * id: item.id,
399
+ * description: 'Updated description',
400
+ * metadata: { extraKey: 'value' }, // merged into existing metadata
401
+ * });
402
+ * ```
403
+ */
404
+ update: async (params) => {
405
+ const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
406
+ return hotMeshClient.engine.store.updateEscalation(params);
337
407
  },
408
+ /**
409
+ * Appends one or more milestone entries to the escalation's
410
+ * `milestones` audit trail array. Milestones are append-only; they
411
+ * record events like state transitions, reviewer notes, or external
412
+ * system callbacks.
413
+ *
414
+ * @example
415
+ * ```typescript
416
+ * await client.escalations.appendMilestones({
417
+ * id: item.id,
418
+ * milestones: [{ at: new Date().toISOString(), by: 'alice', note: 'Reviewed' }],
419
+ * });
420
+ * ```
421
+ */
422
+ appendMilestones: async (params) => {
423
+ const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
424
+ return hotMeshClient.engine.store.appendEscalationMilestones(params);
425
+ },
426
+ /**
427
+ * Atomically claims an escalation row by UUID. Sets `assigned_to`,
428
+ * `claimed_at`, and `claim_expires_at`. Returns `conflict` if another
429
+ * actor already holds the claim.
430
+ *
431
+ * @example
432
+ * ```typescript
433
+ * const result = await client.escalations.claim({
434
+ * id: item.id,
435
+ * assignee: 'alice@company.com',
436
+ * durationMinutes: 30,
437
+ * });
438
+ * if (!result.ok) console.warn('Already claimed by someone else');
439
+ * ```
440
+ */
338
441
  claim: async (params) => {
339
- const hotMesh = await this.getHotMeshClient(null, params.namespace);
340
- const store = hotMesh.engine.store;
341
- if (typeof store.claimSignal !== 'function') {
342
- return { ok: false, reason: 'not-found' };
343
- }
344
- const ns = params.namespace ?? factory_1.APP_ID;
345
- return store.claimSignal({
346
- namespace: ns,
347
- appId: store.appId,
348
- id: params.id,
349
- assignee: params.assignee,
350
- durationMinutes: params.durationMinutes,
351
- });
442
+ const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
443
+ return hotMeshClient.engine.store.claimEscalation(params);
352
444
  },
445
+ /**
446
+ * Atomically claims the highest-priority pending escalation whose
447
+ * `metadata` contains the given key/value pair. Uses
448
+ * `FOR UPDATE SKIP LOCKED` so concurrent callers never double-claim.
449
+ *
450
+ * Returns `candidatesExist` to distinguish two cases:
451
+ * - `not-found, candidatesExist: 0` — no rows matched the metadata filter
452
+ * - `conflict, candidatesExist: N` — matching rows exist but all are claimed
453
+ *
454
+ * @example
455
+ * ```typescript
456
+ * const result = await client.escalations.claimByMetadata({
457
+ * key: 'region',
458
+ * value: 'west',
459
+ * assignee: 'bob@company.com',
460
+ * roles: ['manager'],
461
+ * });
462
+ * if (result.ok) console.log('Claimed:', result.entry.id);
463
+ * ```
464
+ */
353
465
  claimByMetadata: async (params) => {
354
- const hotMesh = await this.getHotMeshClient(null, params.namespace);
355
- const store = hotMesh.engine.store;
356
- if (typeof store.claimSignalByMetadata !== 'function') {
357
- return { ok: false, reason: 'not-found' };
358
- }
359
- const ns = params.namespace ?? factory_1.APP_ID;
360
- return store.claimSignalByMetadata({
361
- namespace: ns,
362
- appId: store.appId,
363
- key: params.key,
364
- value: params.value,
365
- assignee: params.assignee,
366
- durationMinutes: params.durationMinutes,
367
- });
466
+ const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
467
+ return hotMeshClient.engine.store.claimEscalationByMetadata(params);
368
468
  },
469
+ /**
470
+ * Releases a claimed escalation, returning it to `pending` status and
471
+ * clearing `assigned_to` and `claim_expires_at`. The row is immediately
472
+ * available for other actors to claim.
473
+ *
474
+ * @example
475
+ * ```typescript
476
+ * await client.escalations.release({ id: item.id });
477
+ * ```
478
+ */
369
479
  release: async (params) => {
370
- const hotMesh = await this.getHotMeshClient(null, params.namespace);
371
- const store = hotMesh.engine.store;
372
- if (typeof store.releaseSignal !== 'function') {
373
- return { ok: false, reason: 'not-found' };
374
- }
375
- const ns = params.namespace ?? factory_1.APP_ID;
376
- return store.releaseSignal({
377
- namespace: ns,
378
- appId: store.appId,
379
- id: params.id,
380
- });
480
+ const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
481
+ return hotMeshClient.engine.store.releaseEscalation(params);
482
+ },
483
+ /**
484
+ * Reassigns the escalation to a different role, clearing any current
485
+ * claim and returning status to `pending`. Use when an escalation must
486
+ * be handled by a different team or tier.
487
+ *
488
+ * @example
489
+ * ```typescript
490
+ * await client.escalations.escalateToRole({ id: item.id, role: 'senior-manager' });
491
+ * ```
492
+ */
493
+ escalateToRole: async (params) => {
494
+ const hotMeshClient = await this.getHotMeshClient(null, params.namespace);
495
+ return hotMeshClient.engine.store.escalateEscalationToRole(params);
496
+ },
497
+ /**
498
+ * Terminates the escalation without delivering a signal. Rows in
499
+ * `pending` or `claimed` state move to `cancelled`. Terminal rows
500
+ * (`resolved`, `cancelled`) return `already-terminal`.
501
+ *
502
+ * @example
503
+ * ```typescript
504
+ * await client.escalations.cancel(item.id);
505
+ * ```
506
+ */
507
+ cancel: async (id, namespace) => {
508
+ const hotMeshClient = await this.getHotMeshClient(null, namespace);
509
+ return hotMeshClient.engine.store.cancelEscalation(id, namespace);
381
510
  },
382
- resolve: async (params) => {
383
- const hotMesh = await this.getHotMeshClient(null, params.namespace);
384
- const store = hotMesh.engine.store;
385
- if (typeof store.resolveSignal !== 'function') {
511
+ /**
512
+ * Atomically marks the escalation `resolved` **and** delivers the
513
+ * signal to the waiting workflow — one round-trip, no separate
514
+ * `signal()` call required. If `signal_key` is null (standalone
515
+ * escalation), only the row is updated.
516
+ *
517
+ * @example
518
+ * ```typescript
519
+ * const result = await client.escalations.resolve({
520
+ * id: item.id,
521
+ * resolverPayload: { approved: true, note: 'LGTM' },
522
+ * });
523
+ * if (!result.ok) console.error(result.reason); // 'not-found' | 'already-resolved' | 'signal-failed'
524
+ * // workflow resumes with { approved: true, note: 'LGTM' }
525
+ * ```
526
+ */
527
+ resolve: async (params, namespace) => {
528
+ const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
529
+ const hotMeshClient = await this.getHotMeshClient(null, ns);
530
+ const store = hotMeshClient.engine.store;
531
+ // UUID primary-key lookup — no namespace filter needed; use existing.namespace for the UPDATE.
532
+ const existing = await store.getEscalation(params.id);
533
+ if (!existing)
386
534
  return { ok: false, reason: 'not-found' };
535
+ if (existing.status === 'resolved')
536
+ return { ok: false, reason: 'already-resolved' };
537
+ if (existing.status === 'cancelled')
538
+ return { ok: false, reason: 'already-cancelled' };
539
+ // Build a single atomic transaction: signal stream INSERTs + escalation UPDATE in one BEGIN/COMMIT.
540
+ // engine.signal() with a transaction queues an INSERT into the stream table without executing;
541
+ // queueResolveEscalation() queues the status UPDATE; txn.exec() commits all in one round-trip.
542
+ const txn = store.transact();
543
+ if (existing.signal_key && existing.topic) {
544
+ const signalPayload = { id: existing.signal_key, data: params.resolverPayload ?? {} };
545
+ try {
546
+ const sc = await this.getHotMeshClient(`${ns}.wfs.signal`, ns);
547
+ await sc.engine.signal(`${ns}.wfs.signal`, signalPayload, types_1.StreamStatus.SUCCESS, 200, txn);
548
+ }
549
+ catch { /* no collator hook rule — skip */ }
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 */ }
387
555
  }
388
- const ns = params.namespace ?? factory_1.APP_ID;
389
- const storeResult = await store.resolveSignal({
390
- namespace: ns,
391
- appId: store.appId,
392
- id: params.id,
393
- resolverPayload: params.resolverPayload,
394
- });
395
- if (!storeResult.ok)
396
- return storeResult;
397
- try {
398
- await this.workflow.signal(storeResult.signalKey, params.resolverPayload ?? {}, params.namespace);
399
- return { ok: true };
400
- }
401
- catch {
402
- return { ok: false, reason: 'signal-failed', signalKey: storeResult.signalKey };
403
- }
556
+ store.queueResolveEscalation({ id: params.id, namespace: existing.namespace ?? ns, resolverPayload: params.resolverPayload }, txn);
557
+ await txn.exec();
558
+ return { ok: true };
404
559
  },
405
- resolveByMetadata: async (params) => {
406
- const hotMesh = await this.getHotMeshClient(null, params.namespace);
407
- const store = hotMesh.engine.store;
408
- if (typeof store.resolveSignalByMetadata !== 'function') {
560
+ /**
561
+ * Resolves the highest-priority matching escalation by metadata filter,
562
+ * then delivers its signal. Identical semantics to `resolve()` but
563
+ * selects the target row by metadata key/value instead of UUID.
564
+ *
565
+ * @example
566
+ * ```typescript
567
+ * await client.escalations.resolveByMetadata({
568
+ * key: 'orderId',
569
+ * value: 'order-123',
570
+ * resolverPayload: { approved: true },
571
+ * });
572
+ * ```
573
+ */
574
+ resolveByMetadata: async (params, namespace) => {
575
+ const ns = (params.namespace ?? namespace) ?? factory_1.APP_ID;
576
+ const hotMeshClient = await this.getHotMeshClient(null, ns);
577
+ const store = hotMeshClient.engine.store;
578
+ // Metadata lookup: filter by namespace to scope correctly, but use existing.namespace for the UPDATE.
579
+ const existing = await store.findEscalationByMetadata(params.key, params.value, params.roles ?? null);
580
+ if (!existing)
409
581
  return { ok: false, reason: 'not-found' };
582
+ if (existing.status === 'resolved')
583
+ return { ok: false, reason: 'already-resolved' };
584
+ if (existing.status === 'cancelled')
585
+ return { ok: false, reason: 'already-cancelled' };
586
+ // Same atomic pattern as resolve(): signal INSERTs + UPDATE in one transaction.
587
+ const txn = store.transact();
588
+ if (existing.signal_key && existing.topic) {
589
+ const signalPayload = { id: existing.signal_key, data: params.resolverPayload ?? {} };
590
+ try {
591
+ const sc = await this.getHotMeshClient(`${ns}.wfs.signal`, ns);
592
+ await sc.engine.signal(`${ns}.wfs.signal`, signalPayload, types_1.StreamStatus.SUCCESS, 200, txn);
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 */ }
410
600
  }
411
- const ns = params.namespace ?? factory_1.APP_ID;
412
- const storeResult = await store.resolveSignalByMetadata({
413
- namespace: ns,
414
- appId: store.appId,
415
- key: params.key,
416
- value: params.value,
417
- resolverPayload: params.resolverPayload,
418
- });
419
- if (!storeResult.ok)
420
- return storeResult;
421
- try {
422
- await this.workflow.signal(storeResult.signalKey, params.resolverPayload ?? {}, params.namespace);
423
- return { ok: true };
424
- }
425
- catch {
426
- return { ok: false, reason: 'signal-failed', signalKey: storeResult.signalKey };
427
- }
601
+ store.queueResolveEscalation({ id: existing.id, namespace: existing.namespace ?? ns, resolverPayload: params.resolverPayload }, txn);
602
+ await txn.exec();
603
+ return { ok: true };
428
604
  },
605
+ /**
606
+ * Releases all claimed escalations whose `claim_expires_at` has lapsed,
607
+ * returning them to `pending` so they can be claimed again. Returns the
608
+ * number of rows released. Call periodically from a maintenance job or
609
+ * cron to prevent stale claims from blocking the queue.
610
+ *
611
+ * @example
612
+ * ```typescript
613
+ * const released = await client.escalations.releaseExpired();
614
+ * console.log(`Released ${released} expired claims`);
615
+ * ```
616
+ */
429
617
  releaseExpired: async (namespace) => {
430
- const hotMesh = await this.getHotMeshClient(null, namespace);
431
- const store = hotMesh.engine.store;
432
- if (typeof store.releaseExpiredSignals !== 'function')
433
- return 0;
434
- const ns = namespace ?? factory_1.APP_ID;
435
- return store.releaseExpiredSignals({ namespace: ns, appId: store.appId });
618
+ const hotMeshClient = await this.getHotMeshClient(null, namespace);
619
+ return hotMeshClient.engine.store.releaseExpiredEscalations(namespace);
436
620
  },
437
621
  };
438
622
  this.connection = config.connection;
@@ -318,5 +318,3 @@ declare class DurableClass {
318
318
  }
319
319
  export { DurableClass as Durable };
320
320
  export type { ContextType };
321
- export type { ConditionQueueConfig } from './workflow/condition';
322
- export type { ClaimSignalResult, ReleaseSignalResult, ResolveSignalResult, SignalQueueEntry, } from '../../types/signal';
@@ -336,6 +336,9 @@ const getWorkflowYAML = (app, version) => {
336
336
  type: string
337
337
  originJobId:
338
338
  type: string
339
+ queueConfig:
340
+ type: object
341
+ description: optional escalation queue config passed to condition()
339
342
  job:
340
343
  maps:
341
344
  response: '{$self.output.data.response}'
@@ -370,6 +373,23 @@ const getWorkflowYAML = (app, version) => {
370
373
  waiter:
371
374
  title: Waits for a matching signal or optional timeout (single condition)
372
375
  type: hook
376
+ escalation:
377
+ type: '{worker.output.data.queueConfig.type}'
378
+ subtype: '{worker.output.data.queueConfig.subtype}'
379
+ entity: '{worker.output.data.queueConfig.entity}'
380
+ description: '{worker.output.data.queueConfig.description}'
381
+ role: '{worker.output.data.queueConfig.role}'
382
+ priority: '{worker.output.data.queueConfig.priority}'
383
+ metadata: '{worker.output.data.queueConfig.metadata}'
384
+ envelope: '{worker.output.data.queueConfig.envelope}'
385
+ originId: '{worker.output.data.queueConfig.originId}'
386
+ parentId: '{worker.output.data.queueConfig.parentId}'
387
+ initiatedBy: '{worker.output.data.queueConfig.initiatedBy}'
388
+ traceId: '{worker.output.data.queueConfig.traceId}'
389
+ spanId: '{worker.output.data.queueConfig.spanId}'
390
+ expiresAt: '{worker.output.data.queueConfig.expiresAt}'
391
+ taskQueue: '{trigger.output.data.taskQueue}'
392
+ workflowType: '{trigger.output.data.workflowName}'
373
393
  sleep: '{worker.output.data.duration}'
374
394
  hook:
375
395
  type: object
@@ -1137,6 +1157,9 @@ const getWorkflowYAML = (app, version) => {
1137
1157
  type: string
1138
1158
  originJobId:
1139
1159
  type: string
1160
+ queueConfig:
1161
+ type: object
1162
+ description: optional escalation queue config passed to condition()
1140
1163
 
1141
1164
  signaler_sleeper:
1142
1165
  title: Pauses a single thread within the worker for a set amount of seconds while the main flow thread and all other subthreads remain active
@@ -1165,6 +1188,23 @@ const getWorkflowYAML = (app, version) => {
1165
1188
  signaler_waiter:
1166
1189
  title: Waits for a matching signal or optional timeout (single condition in signal-in path)
1167
1190
  type: hook
1191
+ escalation:
1192
+ type: '{signaler_worker.output.data.queueConfig.type}'
1193
+ subtype: '{signaler_worker.output.data.queueConfig.subtype}'
1194
+ entity: '{signaler_worker.output.data.queueConfig.entity}'
1195
+ description: '{signaler_worker.output.data.queueConfig.description}'
1196
+ role: '{signaler_worker.output.data.queueConfig.role}'
1197
+ priority: '{signaler_worker.output.data.queueConfig.priority}'
1198
+ metadata: '{signaler_worker.output.data.queueConfig.metadata}'
1199
+ envelope: '{signaler_worker.output.data.queueConfig.envelope}'
1200
+ originId: '{signaler_worker.output.data.queueConfig.originId}'
1201
+ parentId: '{signaler_worker.output.data.queueConfig.parentId}'
1202
+ initiatedBy: '{signaler_worker.output.data.queueConfig.initiatedBy}'
1203
+ traceId: '{signaler_worker.output.data.queueConfig.traceId}'
1204
+ spanId: '{signaler_worker.output.data.queueConfig.spanId}'
1205
+ expiresAt: '{signaler_worker.output.data.queueConfig.expiresAt}'
1206
+ taskQueue: '{trigger.output.data.taskQueue}'
1207
+ workflowType: '{trigger.output.data.workflowName}'
1168
1208
  sleep: '{signaler_worker.output.data.duration}'
1169
1209
  hook:
1170
1210
  type: object
@@ -806,45 +806,22 @@ class WorkerService {
806
806
  const workflowInput = data.data;
807
807
  const execIndex = counter.counter;
808
808
  const { workflowId, workflowDimension, originJobId } = workflowInput;
809
- const payload = interruptionRegistry[0];
810
- //if condition() was called with queueConfig, create a signal queue record
811
- if (payload.queueConfig) {
812
- const store = this.workflowRunner.engine.store;
813
- if (typeof store.enqueueSignal === 'function') {
814
- const ns = config.namespace ?? factory_1.APP_ID;
815
- try {
816
- await store.enqueueSignal({
817
- namespace: ns,
818
- appId: store.appId,
819
- signalKey: payload.signalId,
820
- workflowId,
821
- topic: `${ns}.wfs.wait`,
822
- taskQueue: config.taskQueue ?? payload.queueConfig.taskQueue,
823
- ...payload.queueConfig,
824
- });
825
- }
826
- catch (enqueueErr) {
827
- this.workflowRunner.engine.logger.warn('signal-queue-enqueue-err', {
828
- signalId: payload.signalId,
829
- error: enqueueErr,
830
- });
831
- }
832
- }
833
- }
809
+ const pendingInterruption = interruptionRegistry[0];
834
810
  return withPatchMarkers({
835
811
  status: stream_1.StreamStatus.SUCCESS,
836
812
  code: enums_1.HMSH_CODE_DURABLE_WAIT,
837
813
  metadata: { ...data.metadata },
838
814
  data: {
839
815
  code: enums_1.HMSH_CODE_DURABLE_WAIT,
840
- signalId: interruptionRegistry[0].signalId,
816
+ signalId: pendingInterruption.signalId,
841
817
  index: execIndex,
842
- workflowDimension: interruptionRegistry[0].workflowDimension ||
818
+ workflowDimension: pendingInterruption.workflowDimension ||
843
819
  workflowDimension ||
844
820
  '',
845
- duration: interruptionRegistry[0].duration,
821
+ duration: pendingInterruption.duration,
846
822
  workflowId,
847
823
  originJobId: originJobId || workflowId,
824
+ queueConfig: pendingInterruption.queueConfig ?? null,
848
825
  },
849
826
  });
850
827
  }