@dudousxd/nestjs-durable 0.42.1 → 0.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { WorkflowCtx, SearchAttributes, WorkflowClass, StepError, WorkflowRef, SingletonConfig, SearchAttributesSchema, StateStore, Transport, NamedTransport, ControlPlane, ScheduledWorkflow, RetentionPolicy, QueueConfig, AdmissionBackend, WorkflowInputOf, StartOptions, RunResult, RunGateway, WorkflowEngine, EntityHandler, DurableTopology, RunDetail, RunQuery, RunListItem, RunFacetQuery, RunFacetRow, GroupHealth, RunWaiting, EngineEvent, RunRequest, RunReply, StepInvocation, TenantEvent } from '@dudousxd/nestjs-durable-core';
1
+ import { WorkflowCtx, SearchAttributes, WorkflowClass, StepError, WorkflowRef, SingletonConfig, SearchAttributesSchema, StateStore, Transport, NamedTransport, ControlPlane, ScheduledWorkflow, RetentionPolicy, QueueConfig, AdmissionBackend, WorkflowInputOf, StartOptions, RunResult, RunGateway, WorkflowEngine, EntityHandler, DurableTopology, RunDetail, RunQuery, RunListItem, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, GroupHealth, RunWaiting, EngineEvent, RunRequest, RunReply, StepInvocation, TenantEvent } from '@dudousxd/nestjs-durable-core';
2
2
  export { AttributeFilter, EngineEvent, InferSearchAttributes, RunDetail, RunGateway, RunListItem, RunQuery, RunStatus, RunWaiting, SearchAttributes, SearchAttributesSchema, StepCheckpoint, StepEvent, StepLogger, WorkflowCtx, WorkflowEngine, WorkflowHandler, WorkflowRun, readSearchAttributes } from '@dudousxd/nestjs-durable-core';
3
3
  import { z } from 'zod';
4
4
  import { ConcurrencyOption, StartRunDeps, RunRedisWorkerOptions, RunningWorker, DurableWorkerRuntime } from '@dudousxd/durable-worker';
@@ -900,6 +900,9 @@ declare class ProxyRunGateway implements RunGateway {
900
900
  listRuns(query: RunQuery): Promise<RunListItem[]>;
901
901
  /** Counted on the operator, over the same tenant-scoped predicates its `listRuns` reply obeys. */
902
902
  runFacets(query: RunFacetQuery): Promise<RunFacetRow[]>;
903
+ /** Enumerated on the operator, over the same tenant-scoped predicates its `listRuns` reply obeys —
904
+ * so a tenant's pickers offer its own tenants/tags/attributes and never another's. */
905
+ runValueFacets(axis: RunValueAxis, query: RunFacetQuery, opts?: RunValueFacetOptions): Promise<RunValueFacetRow[]>;
903
906
  /** Round-trips to the operator, which scopes the result to this tenant's own `@<tenant>` groups. */
904
907
  workerHealth(): Promise<GroupHealth[]>;
905
908
  /** Bulk, one request for the whole id list (like `listRuns`, not one request per id). The operator
@@ -1000,6 +1003,14 @@ declare class StoreRunGateway implements RunGateway {
1000
1003
  * listing — correct, but the unbounded read this exists to avoid, so it is the last resort.
1001
1004
  */
1002
1005
  runFacets(query: RunFacetQuery): Promise<RunFacetRow[]>;
1006
+ /**
1007
+ * The distinct values behind a console's pickers, straight off the store's enumeration. A store
1008
+ * without one answers `[]` rather than falling back to a listing: unlike the facet counts above —
1009
+ * where a full listing is at least the RIGHT answer expensively — a picker built from an unbounded
1010
+ * scan would be the same unbounded read on every keystroke, for a control whose free-text entry
1011
+ * still works without it.
1012
+ */
1013
+ runValueFacets(axis: RunValueAxis, query: RunFacetQuery, opts?: RunValueFacetOptions): Promise<RunValueFacetRow[]>;
1003
1014
  /**
1004
1015
  * Bulk-resolve what each of `runIds` is currently parked on — for a consumer with its own filtered/
1005
1016
  * paginated run listing (e.g. "which of MY suspended runs are stuck at a breakpoint") without
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { WorkflowCtx, SearchAttributes, WorkflowClass, StepError, WorkflowRef, SingletonConfig, SearchAttributesSchema, StateStore, Transport, NamedTransport, ControlPlane, ScheduledWorkflow, RetentionPolicy, QueueConfig, AdmissionBackend, WorkflowInputOf, StartOptions, RunResult, RunGateway, WorkflowEngine, EntityHandler, DurableTopology, RunDetail, RunQuery, RunListItem, RunFacetQuery, RunFacetRow, GroupHealth, RunWaiting, EngineEvent, RunRequest, RunReply, StepInvocation, TenantEvent } from '@dudousxd/nestjs-durable-core';
1
+ import { WorkflowCtx, SearchAttributes, WorkflowClass, StepError, WorkflowRef, SingletonConfig, SearchAttributesSchema, StateStore, Transport, NamedTransport, ControlPlane, ScheduledWorkflow, RetentionPolicy, QueueConfig, AdmissionBackend, WorkflowInputOf, StartOptions, RunResult, RunGateway, WorkflowEngine, EntityHandler, DurableTopology, RunDetail, RunQuery, RunListItem, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, GroupHealth, RunWaiting, EngineEvent, RunRequest, RunReply, StepInvocation, TenantEvent } from '@dudousxd/nestjs-durable-core';
2
2
  export { AttributeFilter, EngineEvent, InferSearchAttributes, RunDetail, RunGateway, RunListItem, RunQuery, RunStatus, RunWaiting, SearchAttributes, SearchAttributesSchema, StepCheckpoint, StepEvent, StepLogger, WorkflowCtx, WorkflowEngine, WorkflowHandler, WorkflowRun, readSearchAttributes } from '@dudousxd/nestjs-durable-core';
3
3
  import { z } from 'zod';
4
4
  import { ConcurrencyOption, StartRunDeps, RunRedisWorkerOptions, RunningWorker, DurableWorkerRuntime } from '@dudousxd/durable-worker';
@@ -900,6 +900,9 @@ declare class ProxyRunGateway implements RunGateway {
900
900
  listRuns(query: RunQuery): Promise<RunListItem[]>;
901
901
  /** Counted on the operator, over the same tenant-scoped predicates its `listRuns` reply obeys. */
902
902
  runFacets(query: RunFacetQuery): Promise<RunFacetRow[]>;
903
+ /** Enumerated on the operator, over the same tenant-scoped predicates its `listRuns` reply obeys —
904
+ * so a tenant's pickers offer its own tenants/tags/attributes and never another's. */
905
+ runValueFacets(axis: RunValueAxis, query: RunFacetQuery, opts?: RunValueFacetOptions): Promise<RunValueFacetRow[]>;
903
906
  /** Round-trips to the operator, which scopes the result to this tenant's own `@<tenant>` groups. */
904
907
  workerHealth(): Promise<GroupHealth[]>;
905
908
  /** Bulk, one request for the whole id list (like `listRuns`, not one request per id). The operator
@@ -1000,6 +1003,14 @@ declare class StoreRunGateway implements RunGateway {
1000
1003
  * listing — correct, but the unbounded read this exists to avoid, so it is the last resort.
1001
1004
  */
1002
1005
  runFacets(query: RunFacetQuery): Promise<RunFacetRow[]>;
1006
+ /**
1007
+ * The distinct values behind a console's pickers, straight off the store's enumeration. A store
1008
+ * without one answers `[]` rather than falling back to a listing: unlike the facet counts above —
1009
+ * where a full listing is at least the RIGHT answer expensively — a picker built from an unbounded
1010
+ * scan would be the same unbounded read on every keystroke, for a control whose free-text entry
1011
+ * still works without it.
1012
+ */
1013
+ runValueFacets(axis: RunValueAxis, query: RunFacetQuery, opts?: RunValueFacetOptions): Promise<RunValueFacetRow[]>;
1003
1014
  /**
1004
1015
  * Bulk-resolve what each of `runIds` is currently parked on — for a consumer with its own filtered/
1005
1016
  * paginated run listing (e.g. "which of MY suspended runs are stuck at a breakpoint") without
package/dist/index.js CHANGED
@@ -577,6 +577,9 @@ function unavailableRunGateway() {
577
577
  runFacets(_query) {
578
578
  return tenantGatewayUnavailable("runFacets");
579
579
  },
580
+ runValueFacets(_axis, _query, _opts) {
581
+ return tenantGatewayUnavailable("runValueFacets");
582
+ },
580
583
  waitingFor(_runIds) {
581
584
  return tenantGatewayUnavailable("waitingFor");
582
585
  },
@@ -1006,6 +1009,16 @@ var ProxyRunGateway = class {
1006
1009
  query
1007
1010
  });
1008
1011
  }
1012
+ /** Enumerated on the operator, over the same tenant-scoped predicates its `listRuns` reply obeys —
1013
+ * so a tenant's pickers offer its own tenants/tags/attributes and never another's. */
1014
+ runValueFacets(axis, query, opts) {
1015
+ return this.request({
1016
+ kind: "runValueFacets",
1017
+ axis,
1018
+ query,
1019
+ opts
1020
+ });
1021
+ }
1009
1022
  /** Round-trips to the operator, which scopes the result to this tenant's own `@<tenant>` groups. */
1010
1023
  workerHealth() {
1011
1024
  return this.request({
@@ -1156,6 +1169,15 @@ var RetentionPoller = class {
1156
1169
  onModuleDestroy() {
1157
1170
  if (this.timer) clearInterval(this.timer);
1158
1171
  }
1172
+ /**
1173
+ * Prune one round.
1174
+ *
1175
+ * Never rejects, for the same reason {@link TimerPoller.poll} does not: it is awaited inside
1176
+ * `onApplicationBootstrap` (so a rejection propagates into the host's `bootstrap()` and kills the
1177
+ * process) and fired as `void` on the interval (so a rejection is an unhandled one, also fatal).
1178
+ * Pruning history is the least urgent thing this package does — a `pruneTerminalRuns` that fails
1179
+ * on a locked table must cost this sweep and nothing more.
1180
+ */
1159
1181
  async sweep() {
1160
1182
  if (this.sweeping) return;
1161
1183
  const retention = this.options.retention;
@@ -1172,6 +1194,8 @@ var RetentionPoller = class {
1172
1194
  if (deleted < batchSize) break;
1173
1195
  }
1174
1196
  }
1197
+ } catch (error) {
1198
+ console.error("[nestjs-durable] retention sweep failed; retrying next interval:", error);
1175
1199
  } finally {
1176
1200
  this.sweeping = false;
1177
1201
  }
@@ -1235,6 +1259,19 @@ var RunRequestResponder = class {
1235
1259
  }
1236
1260
  };
1237
1261
  }
1262
+ if (body.kind === "runValueFacets") {
1263
+ const data = await this.gateway.runValueFacets(body.axis, {
1264
+ ...body.query,
1265
+ namespace: msg.tenant
1266
+ }, body.opts);
1267
+ return {
1268
+ requestId: msg.requestId,
1269
+ result: {
1270
+ ok: true,
1271
+ data
1272
+ }
1273
+ };
1274
+ }
1238
1275
  if (body.kind === "workerHealth") {
1239
1276
  const all = await this.gateway.workerHealth();
1240
1277
  const data = all.filter((h) => h.group.endsWith(`@${msg.tenant}`));
@@ -1401,6 +1438,16 @@ var StoreRunGateway = class {
1401
1438
  })));
1402
1439
  }
1403
1440
  /**
1441
+ * The distinct values behind a console's pickers, straight off the store's enumeration. A store
1442
+ * without one answers `[]` rather than falling back to a listing: unlike the facet counts above —
1443
+ * where a full listing is at least the RIGHT answer expensively — a picker built from an unbounded
1444
+ * scan would be the same unbounded read on every keystroke, for a control whose free-text entry
1445
+ * still works without it.
1446
+ */
1447
+ async runValueFacets(axis, query, opts) {
1448
+ return this.store.runValueFacets?.(axis, query, opts) ?? [];
1449
+ }
1450
+ /**
1404
1451
  * Bulk-resolve what each of `runIds` is currently parked on — for a consumer with its own filtered/
1405
1452
  * paginated run listing (e.g. "which of MY suspended runs are stuck at a breakpoint") without
1406
1453
  * re-deriving `listRuns`' waiter scan or querying `durable_step_checkpoints` directly. Mirrors
@@ -1543,7 +1590,11 @@ var TimerPoller = class {
1543
1590
  }
1544
1591
  async onApplicationBootstrap() {
1545
1592
  if (!isDrivingOperator(this.options)) return;
1546
- this.unsubscribeEnqueued = this.engine.onEnqueued((runId) => void this.engine.runOne(runId));
1593
+ this.unsubscribeEnqueued = this.engine.onEnqueued((runId) => {
1594
+ void this.engine.runOne(runId).catch((error) => {
1595
+ console.error(`[nestjs-durable] could not run enqueued run ${runId}:`, error);
1596
+ });
1597
+ });
1547
1598
  await this.poll();
1548
1599
  const intervalMs = this.options.timerPollMs ?? 1e3;
1549
1600
  if (intervalMs > 0) {
@@ -1555,22 +1606,45 @@ var TimerPoller = class {
1555
1606
  if (this.timer) clearInterval(this.timer);
1556
1607
  this.unsubscribeEnqueued?.();
1557
1608
  }
1609
+ /**
1610
+ * One sweep of the engine's recovery work.
1611
+ *
1612
+ * **This method never rejects, and that is a hard requirement rather than defensiveness.** It has
1613
+ * exactly two callers and a rejection out of either one kills the process: on boot it is awaited
1614
+ * inside `onApplicationBootstrap`, so the rejection propagates through `NestApplication.init()`
1615
+ * into the host's `bootstrap()`; on the interval it is a fire-and-forget `void`, so it surfaces as
1616
+ * an unhandled rejection. Both are fatal, and both re-occur on the next boot with the same state,
1617
+ * so a single unrecoverable run crash-loops every worker in the deployment — taking down every
1618
+ * other workflow with it. A poll that goes wrong must cost this tick, not the pod.
1619
+ *
1620
+ * Each sweep is isolated separately because they are independent: a store that fails
1621
+ * `recoverIncomplete` says nothing about whether durable timers should still fire, and skipping
1622
+ * the rest of the tick would silently widen one subsystem's outage into all of them.
1623
+ */
1558
1624
  async poll() {
1559
1625
  if (this.polling) return;
1560
1626
  this.polling = true;
1561
1627
  try {
1562
- await this.engine.runPending();
1563
- await this.engine.recoverIncomplete();
1564
- await this.engine.resumeDueTimers();
1565
- await this.engine.sweepTimeouts();
1628
+ await this.sweep("runPending", () => this.engine.runPending());
1629
+ await this.sweep("recoverIncomplete", () => this.engine.recoverIncomplete());
1630
+ await this.sweep("resumeDueTimers", () => this.engine.resumeDueTimers());
1631
+ await this.sweep("sweepTimeouts", () => this.engine.sweepTimeouts());
1566
1632
  const schedules = this.options.schedules;
1567
1633
  if (schedules && schedules.length > 0) {
1568
- await runSchedules(this.engine, schedules, Date.now());
1634
+ await this.sweep("schedules", () => runSchedules(this.engine, schedules, Date.now()));
1569
1635
  }
1570
1636
  } finally {
1571
1637
  this.polling = false;
1572
1638
  }
1573
1639
  }
1640
+ /** Run one sweep, absorbing and reporting its failure — see {@link poll}. */
1641
+ async sweep(name, run) {
1642
+ try {
1643
+ await run();
1644
+ } catch (error) {
1645
+ console.error(`[nestjs-durable] timer poll: ${name} failed; continuing:`, error);
1646
+ }
1647
+ }
1574
1648
  };
1575
1649
  TimerPoller = _ts_decorate9([
1576
1650
  Injectable9(),