@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.
- package/README.md +12 -129
- package/build/modules/utils.js +3 -0
- package/build/package.json +2 -1
- package/build/services/activities/hook.d.ts +178 -58
- package/build/services/activities/hook.js +244 -58
- package/build/services/activities/trigger.js +5 -1
- package/build/services/durable/client.d.ts +238 -66
- package/build/services/durable/client.js +309 -125
- package/build/services/durable/index.d.ts +0 -2
- package/build/services/durable/schemas/factory.js +40 -0
- package/build/services/durable/worker.js +5 -28
- package/build/services/durable/workflow/condition.d.ts +69 -37
- package/build/services/durable/workflow/condition.js +70 -39
- package/build/services/hotmesh/index.d.ts +31 -4
- package/build/services/hotmesh/index.js +31 -4
- package/build/services/store/index.d.ts +1 -1
- package/build/services/store/providers/postgres/kvsql.d.ts +1 -1
- package/build/services/store/providers/postgres/kvtables.js +83 -122
- package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +1 -1
- package/build/services/store/providers/postgres/kvtypes/hash/basic.js +8 -8
- package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +1 -1
- package/build/services/store/providers/postgres/postgres.d.ts +44 -157
- package/build/services/store/providers/postgres/postgres.js +480 -278
- package/build/types/activity.d.ts +2 -0
- package/build/types/hmsh_escalations.d.ts +212 -0
- package/build/types/index.d.ts +1 -1
- package/build/types/provider.d.ts +2 -0
- package/package.json +2 -1
- package/build/types/signal.d.ts +0 -147
- /package/build/types/{signal.js → hmsh_escalations.js} +0 -0
|
@@ -285,154 +285,338 @@ class ClientService {
|
|
|
285
285
|
},
|
|
286
286
|
};
|
|
287
287
|
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
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
|
-
*
|
|
293
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* //
|
|
298
|
-
* const
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
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
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
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.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
340
|
-
|
|
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
|
|
355
|
-
|
|
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
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
431
|
-
|
|
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
|
|
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:
|
|
816
|
+
signalId: pendingInterruption.signalId,
|
|
841
817
|
index: execIndex,
|
|
842
|
-
workflowDimension:
|
|
818
|
+
workflowDimension: pendingInterruption.workflowDimension ||
|
|
843
819
|
workflowDimension ||
|
|
844
820
|
'',
|
|
845
|
-
duration:
|
|
821
|
+
duration: pendingInterruption.duration,
|
|
846
822
|
workflowId,
|
|
847
823
|
originJobId: originJobId || workflowId,
|
|
824
|
+
queueConfig: pendingInterruption.queueConfig ?? null,
|
|
848
825
|
},
|
|
849
826
|
});
|
|
850
827
|
}
|