@dbos-inc/dbos-sdk 5.1.5-preview → 5.1.8-preview

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 (63) hide show
  1. package/dist/schemas/system_db_schema.d.ts +1 -0
  2. package/dist/schemas/system_db_schema.d.ts.map +1 -1
  3. package/dist/schemas/system_db_schema.js.map +1 -1
  4. package/dist/src/client.d.ts +17 -0
  5. package/dist/src/client.d.ts.map +1 -1
  6. package/dist/src/client.js +20 -4
  7. package/dist/src/client.js.map +1 -1
  8. package/dist/src/conductor/conductor.d.ts.map +1 -1
  9. package/dist/src/conductor/conductor.js +18 -0
  10. package/dist/src/conductor/conductor.js.map +1 -1
  11. package/dist/src/conductor/protocol.d.ts +17 -1
  12. package/dist/src/conductor/protocol.d.ts.map +1 -1
  13. package/dist/src/conductor/protocol.js +10 -1
  14. package/dist/src/conductor/protocol.js.map +1 -1
  15. package/dist/src/context.d.ts +12 -1
  16. package/dist/src/context.d.ts.map +1 -1
  17. package/dist/src/context.js +5 -1
  18. package/dist/src/context.js.map +1 -1
  19. package/dist/src/datasource.d.ts +6 -0
  20. package/dist/src/datasource.d.ts.map +1 -1
  21. package/dist/src/datasource.js.map +1 -1
  22. package/dist/src/dbos-executor.d.ts +10 -0
  23. package/dist/src/dbos-executor.d.ts.map +1 -1
  24. package/dist/src/dbos-executor.js +87 -55
  25. package/dist/src/dbos-executor.js.map +1 -1
  26. package/dist/src/dbos.d.ts +27 -1
  27. package/dist/src/dbos.d.ts.map +1 -1
  28. package/dist/src/dbos.js +29 -0
  29. package/dist/src/dbos.js.map +1 -1
  30. package/dist/src/debouncer.d.ts.map +1 -1
  31. package/dist/src/debouncer.js +3 -0
  32. package/dist/src/debouncer.js.map +1 -1
  33. package/dist/src/enqueue_options.d.ts +3 -1
  34. package/dist/src/enqueue_options.d.ts.map +1 -1
  35. package/dist/src/enqueue_options.js.map +1 -1
  36. package/dist/src/enqueue_workflow.d.ts.map +1 -1
  37. package/dist/src/enqueue_workflow.js +11 -6
  38. package/dist/src/enqueue_workflow.js.map +1 -1
  39. package/dist/src/error.d.ts +10 -0
  40. package/dist/src/error.d.ts.map +1 -1
  41. package/dist/src/error.js +25 -1
  42. package/dist/src/error.js.map +1 -1
  43. package/dist/src/index.d.ts +1 -1
  44. package/dist/src/index.d.ts.map +1 -1
  45. package/dist/src/index.js.map +1 -1
  46. package/dist/src/step.d.ts +1 -0
  47. package/dist/src/step.d.ts.map +1 -1
  48. package/dist/src/step.js.map +1 -1
  49. package/dist/src/sysdb_migrations/internal/migrations.js +6 -0
  50. package/dist/src/sysdb_migrations/internal/migrations.js.map +1 -1
  51. package/dist/src/system_database.d.ts +41 -2
  52. package/dist/src/system_database.d.ts.map +1 -1
  53. package/dist/src/system_database.js +231 -17
  54. package/dist/src/system_database.js.map +1 -1
  55. package/dist/src/workflow.d.ts +3 -0
  56. package/dist/src/workflow.d.ts.map +1 -1
  57. package/dist/src/workflow.js.map +1 -1
  58. package/dist/src/workflow_management.d.ts +15 -0
  59. package/dist/src/workflow_management.d.ts.map +1 -1
  60. package/dist/src/workflow_management.js +45 -1
  61. package/dist/src/workflow_management.js.map +1 -1
  62. package/dist/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +1 -1
@@ -15,6 +15,7 @@ const pg_1 = require("pg");
15
15
  const error_1 = require("./error");
16
16
  const workflow_1 = require("./workflow");
17
17
  const utils_1 = require("./utils");
18
+ const async_hooks_1 = require("async_hooks");
18
19
  const crypto_1 = require("crypto");
19
20
  const utils_2 = require("./utils");
20
21
  const database_utils_1 = require("./database_utils");
@@ -564,6 +565,7 @@ class SystemDatabase {
564
565
  dbPollingIntervalResultMs = 1000;
565
566
  dbPollingIntervalEventMs = 10000;
566
567
  dbPollingIntervalStreamMs = 1000;
568
+ dbPollingIntervalCancelMs = 1000;
567
569
  shouldUseDBNotifications = true;
568
570
  notificationsMap = new NotificationMap();
569
571
  workflowEventsMap = new NotificationMap();
@@ -587,6 +589,11 @@ class SystemDatabase {
587
589
  */
588
590
  pollLimiter;
589
591
  runningWorkflowMap = new Map(); // Map from workflowID to workflow promise, queue name and partition key
592
+ #cancelWatchers = new Map();
593
+ #cancelPollerLoop = undefined;
594
+ #cancelPollerWake = null;
595
+ // Runs the lazily started cancel poller outside the async context of the step that happens to start it
596
+ #cancelPollerScope = new async_hooks_1.AsyncResource('DBOSCancelPoller');
590
597
  // Per-partition-key created_at cursors: keep per-key queue order monotonic across batches
591
598
  #batchCreatedAtCursors = new Map();
592
599
  // Set by destroy(), so polling waits end instead of running on against a pool that outlives this handle.
@@ -773,6 +780,10 @@ class SystemDatabase {
773
780
  await this.#notifierLoop;
774
781
  this.#notifierLoop = undefined;
775
782
  }
783
+ this.#cancelPollerWake?.();
784
+ if (this.#cancelPollerLoop) {
785
+ await this.#cancelPollerLoop;
786
+ }
776
787
  if (this.notificationsClient) {
777
788
  this.#retireNotificationsClient(this.notificationsClient);
778
789
  }
@@ -797,18 +808,18 @@ class SystemDatabase {
797
808
  }
798
809
  // ==================== Workflow Status ====================
799
810
  /** Runs on `client` if given, joining its transaction; otherwise in its own retried transaction. */
800
- async initWorkflowStatus(initStatus, ownerXid, client) {
811
+ async initWorkflowStatus(initStatus, ownerXid, client, reusePolicy = 'return-existing') {
801
812
  if (client !== undefined) {
802
- return await this.#initWorkflowStatusInternal(client, initStatus, ownerXid);
813
+ return await this.#initWorkflowStatusInternal(client, initStatus, ownerXid, reusePolicy);
803
814
  }
804
- return await this.initWorkflowStatusStandalone(initStatus, ownerXid);
815
+ return await this.initWorkflowStatusStandalone(initStatus, ownerXid, reusePolicy);
805
816
  }
806
- async initWorkflowStatusStandalone(initStatus, ownerXid) {
817
+ async initWorkflowStatusStandalone(initStatus, ownerXid, reusePolicy) {
807
818
  const client = await this.#connect();
808
819
  let shouldCommit = false;
809
820
  try {
810
821
  await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
811
- const result = await this.#initWorkflowStatusInternal(client, initStatus, ownerXid);
822
+ const result = await this.#initWorkflowStatusInternal(client, initStatus, ownerXid, reusePolicy);
812
823
  // If there is an existing DB record and we aren't here to recover it, leave it be.
813
824
  shouldCommit = result.shouldExecuteOnThisExecutor;
814
825
  return result;
@@ -828,8 +839,37 @@ class SystemDatabase {
828
839
  }
829
840
  }
830
841
  }
831
- async #initWorkflowStatusInternal(client, initStatus, ownerXid) {
842
+ /** Insert a child workflow's row and record it as the parent's step in one transaction, so a crash leaves both or neither. */
843
+ async initChildWorkflowStatus(initStatus, ownerXid, parentWorkflowID, parentFunctionID, startTimeEpochMs, endTimeEpochMs, reusePolicy = 'return-existing') {
844
+ const client = await this.#connect();
845
+ let shouldCommit = false;
846
+ try {
847
+ await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
848
+ const result = await this.#initWorkflowStatusInternal(client, initStatus, ownerXid, reusePolicy);
849
+ await this.recordOperationResultInternal(client, parentWorkflowID, parentFunctionID, initStatus.workflowName, true, startTimeEpochMs, endTimeEpochMs, { childWorkflowID: initStatus.workflowUUID });
850
+ shouldCommit = true;
851
+ return result;
852
+ }
853
+ finally {
854
+ try {
855
+ if (shouldCommit) {
856
+ await client.query('COMMIT');
857
+ await (0, debugpoint_1.debugTriggerPoint)(debugpoint_1.DEBUG_TRIGGER_INITWF_COMMIT);
858
+ }
859
+ else {
860
+ await client.query('ROLLBACK');
861
+ }
862
+ }
863
+ finally {
864
+ client.release();
865
+ }
866
+ }
867
+ }
868
+ async #initWorkflowStatusInternal(client, initStatus, ownerXid, reusePolicy) {
832
869
  const resRow = await this.insertWorkflowStatus(client, initStatus, ownerXid);
870
+ if (reusePolicy === 'reject' && (ownerXid === null || resRow.owner_xid !== ownerXid)) {
871
+ throw new error_1.DBOSWorkflowIDInUseError(initStatus.workflowUUID, resRow.status, resRow.name);
872
+ }
833
873
  if (resRow.name !== initStatus.workflowName) {
834
874
  const msg = `Workflow already exists with a different function name: ${resRow.name}, but the provided function name is: ${initStatus.workflowName}`;
835
875
  throw new error_1.DBOSConflictingWorkflowError(initStatus.workflowUUID, msg);
@@ -1464,6 +1504,106 @@ class SystemDatabase {
1464
1504
  this.runningWorkflowMap.delete(wfid);
1465
1505
  }
1466
1506
  }
1507
+ /**
1508
+ * Drop a workflow's history from `startStep` onwards and re-enqueue it under the same
1509
+ * workflow ID, so a replay re-executes everything from that step.
1510
+ *
1511
+ * Unlike forkWorkflow this writes no new workflow: peers keep addressing the same ID,
1512
+ * and the workflow's mailbox, events, and streams are not copied anywhere. Unlike
1513
+ * resumeWorkflows it applies only to workflows in a terminal state.
1514
+ *
1515
+ * Events published at or past the cut are rolled back to the last value published
1516
+ * below it, using workflow_events_history as an undo log; a key that only the
1517
+ * discarded run ever published is unpublished outright.
1518
+ *
1519
+ * Messages the discarded run consumed or received after the rewind point are deleted.
1520
+ *
1521
+ * Stream entries written by the discarded run remain in place, with the exception of
1522
+ * the close sentinel, which has to go so new entries can be appended.
1523
+ */
1524
+ async rewindWorkflow(workflowID, startStep, options = {}) {
1525
+ // Function IDs start at 0 in this SDK, so 0 is the whole history, not an error.
1526
+ if (startStep < 0) {
1527
+ throw new error_1.DBOSError(`startStep must be >= 0, got ${startStep}`);
1528
+ }
1529
+ const schema = this.schemaName;
1530
+ const client = await this.#connect();
1531
+ try {
1532
+ await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
1533
+ const { rows: statusRows } = await client.query(`SELECT status FROM "${schema}".workflow_status WHERE workflow_uuid = $1`, [workflowID]);
1534
+ if (statusRows.length === 0) {
1535
+ throw new error_1.DBOSNonExistentWorkflowError(`Workflow ${workflowID} does not exist`);
1536
+ }
1537
+ const status = statusRows[0].status;
1538
+ if (status === workflow_1.StatusString.PENDING || status === workflow_1.StatusString.ENQUEUED || status === workflow_1.StatusString.DELAYED) {
1539
+ throw new error_1.DBOSError(`Cannot rewind ${workflowID} (${status}): only a workflow in a terminal state can be rewound, so cancel it first`);
1540
+ }
1541
+ // Roll workflow_events back to the last value published before the cut, using
1542
+ // workflow_events_history as an undo log, then rewind that history too.
1543
+ // `publishedPastCut` is the correlated EXISTS both halves key off.
1544
+ const publishedPastCut = `EXISTS (
1545
+ SELECT 1 FROM "${schema}".workflow_events_history discarded
1546
+ WHERE discarded.workflow_uuid = $1 AND discarded.key = %KEY% AND discarded.function_id >= $2
1547
+ )`;
1548
+ // First unpublish everything the discarded run touched.
1549
+ await client.query(`DELETE FROM "${schema}".workflow_events
1550
+ WHERE workflow_uuid = $1 AND ${publishedPastCut.replace('%KEY%', '"' + schema + '".workflow_events.key')}`, [workflowID, startStep]);
1551
+ // Then restore those keys, if any, to the value published below the cut.
1552
+ await client.query(`INSERT INTO "${schema}".workflow_events (workflow_uuid, key, value, serialization)
1553
+ SELECT surviving.workflow_uuid, surviving.key, surviving.value, surviving.serialization
1554
+ FROM (
1555
+ SELECT weh.workflow_uuid, weh.key, weh.value, weh.serialization,
1556
+ ROW_NUMBER() OVER (PARTITION BY weh.key ORDER BY weh.function_id DESC) AS rn
1557
+ FROM "${schema}".workflow_events_history weh
1558
+ WHERE weh.workflow_uuid = $1 AND weh.function_id < $2
1559
+ AND ${publishedPastCut.replace('%KEY%', 'weh.key')}
1560
+ ) surviving
1561
+ WHERE surviving.rn = 1`, [workflowID, startStep]);
1562
+ // Clear the streams' close sentinels so the replay can append again.
1563
+ await client.query(`DELETE FROM "${schema}".streams
1564
+ WHERE workflow_uuid = $1 AND function_id >= $2 AND value = $3 AND serialization = $4`, [workflowID, startStep, exports.DBOS_STREAM_CLOSED_SENTINEL_SERIALIZED, serialization_1.DBOSPortableJSON.name()]);
1565
+ // Discard the steps and the events history past the cut.
1566
+ for (const table of ['operation_outputs', 'workflow_events_history']) {
1567
+ await client.query(`DELETE FROM "${schema}".${table} WHERE workflow_uuid = $1 AND function_id >= $2`, [
1568
+ workflowID,
1569
+ startStep,
1570
+ ]);
1571
+ }
1572
+ // Delete messages consumed or received after the rewind point
1573
+ await client.query(`DELETE FROM "${schema}".notifications WHERE destination_uuid = $1 AND (consumed_by_function_id >= $2 OR consumed = false)`, [workflowID, startStep]);
1574
+ await client.query(`DELETE FROM "${schema}".workflow_output WHERE workflow_uuid = $1`, [workflowID]);
1575
+ // Re-enqueue the workflow. Re-asserting the status we read keeps a workflow that
1576
+ // moved on underneath us from being resurrected.
1577
+ const setVersion = options.applicationVersion !== undefined ? ', application_version = $6' : '';
1578
+ const params = [
1579
+ workflow_1.StatusString.ENQUEUED,
1580
+ options.queueName ?? utils_1.INTERNAL_QUEUE_NAME,
1581
+ options.queuePartitionKey ?? null,
1582
+ workflowID,
1583
+ status,
1584
+ ];
1585
+ if (options.applicationVersion !== undefined) {
1586
+ params.push(options.applicationVersion);
1587
+ }
1588
+ const { rowCount } = await client.query(`UPDATE "${schema}".workflow_status
1589
+ SET status = $1, queue_name = $2, queue_partition_key = $3, recovery_attempts = 0,
1590
+ workflow_deadline_epoch_ms = NULL, deduplication_id = NULL, started_at_epoch_ms = NULL,
1591
+ completed_at = NULL, output = NULL, error = NULL,
1592
+ updated_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint${setVersion}
1593
+ WHERE workflow_uuid = $4 AND status = $5`, params);
1594
+ if (rowCount !== 1) {
1595
+ throw new error_1.DBOSError(`Workflow ${workflowID} changed status while being rewound; retry the rewind`);
1596
+ }
1597
+ await client.query('COMMIT');
1598
+ }
1599
+ catch (e) {
1600
+ await client.query('ROLLBACK').catch(() => undefined);
1601
+ throw e;
1602
+ }
1603
+ finally {
1604
+ client.release();
1605
+ }
1606
+ }
1467
1607
  async forkWorkflow(workflowID, startStep, options = {}) {
1468
1608
  const newWorkflowID = options.newWorkflowID ?? (0, crypto_1.randomUUID)();
1469
1609
  const result = await this.bulkForkWorkflows([workflowID], [newWorkflowID], [startStep], options);
@@ -1961,6 +2101,74 @@ class SystemDatabase {
1961
2101
  async checkIfCanceledLimited(workflowID) {
1962
2102
  await this.#pollWithLimiter(() => this.#checkIfCanceled(this.pool, workflowID));
1963
2103
  }
2104
+ watchForCancellation(workflowID) {
2105
+ const controller = new AbortController();
2106
+ let state = 'idle';
2107
+ const start = () => {
2108
+ state = 'watching';
2109
+ if (this.#destroyed)
2110
+ return;
2111
+ let watchers = this.#cancelWatchers.get(workflowID);
2112
+ if (watchers === undefined) {
2113
+ watchers = new Set();
2114
+ this.#cancelWatchers.set(workflowID, watchers);
2115
+ }
2116
+ watchers.add(controller);
2117
+ this.#cancelPollerLoop ??= this.#cancelPollerScope.runInAsyncScope(() => this.#runCancelPoller());
2118
+ };
2119
+ const stop = () => {
2120
+ const watchers = this.#cancelWatchers.get(workflowID);
2121
+ if (watchers?.delete(controller) && watchers.size === 0) {
2122
+ this.#cancelWatchers.delete(workflowID);
2123
+ if (this.#cancelWatchers.size === 0)
2124
+ this.#cancelPollerWake?.();
2125
+ }
2126
+ };
2127
+ return {
2128
+ get signal() {
2129
+ if (state === 'idle')
2130
+ start();
2131
+ return controller.signal;
2132
+ },
2133
+ get cancelled() {
2134
+ return controller.signal.aborted;
2135
+ },
2136
+ release: () => {
2137
+ if (state === 'watching')
2138
+ stop();
2139
+ state = 'released';
2140
+ },
2141
+ };
2142
+ }
2143
+ async #runCancelPoller() {
2144
+ while (this.#cancelWatchers.size > 0 && !this.#destroyed) {
2145
+ const { promise, cancel } = (0, utils_1.cancellableSleep)(this.dbPollingIntervalCancelMs);
2146
+ this.#cancelPollerWake = cancel;
2147
+ await promise;
2148
+ this.#cancelPollerWake = null;
2149
+ if (this.#cancelWatchers.size === 0 || this.#destroyed)
2150
+ break;
2151
+ try {
2152
+ await this.#abortCancelledWatchers();
2153
+ }
2154
+ catch (e) {
2155
+ this.logger.debug(`Workflow cancellation poll failed: ${String(e)}`);
2156
+ }
2157
+ }
2158
+ this.#cancelPollerLoop = undefined;
2159
+ }
2160
+ async #abortCancelledWatchers() {
2161
+ const { rows } = await this.#pollWithLimiter(() => this.pool.query(`SELECT workflow_uuid FROM "${this.schemaName}".workflow_status WHERE workflow_uuid = ANY($1) AND status = $2`, [Array.from(this.#cancelWatchers.keys()), workflow_1.StatusString.CANCELLED]));
2162
+ for (const { workflow_uuid: workflowID } of rows) {
2163
+ const watchers = this.#cancelWatchers.get(workflowID);
2164
+ if (watchers === undefined)
2165
+ continue;
2166
+ this.#cancelWatchers.delete(workflowID);
2167
+ const reason = new error_1.DBOSWorkflowCancelledError(workflowID);
2168
+ for (const controller of watchers)
2169
+ controller.abort(reason);
2170
+ }
2171
+ }
1964
2172
  // A missing row normally means the workflow has not been inserted yet, so
1965
2173
  // polling for it is correct. Callers that know the row must already exist
1966
2174
  // (e.g. a run parking on an outcome it just failed to write) pass
@@ -2179,7 +2387,7 @@ class SystemDatabase {
2179
2387
  try {
2180
2388
  await client.query(`BEGIN ISOLATION LEVEL READ COMMITTED`);
2181
2389
  const finalRecvRows = (await client.query(`UPDATE "${this.schemaName}".notifications
2182
- SET consumed = true
2390
+ SET consumed = true, consumed_by_function_id = $3
2183
2391
  WHERE destination_uuid = $1
2184
2392
  AND topic = $2
2185
2393
  AND consumed = false
@@ -2192,7 +2400,7 @@ class SystemDatabase {
2192
2400
  ORDER BY created_at_epoch_ms ASC
2193
2401
  LIMIT 1
2194
2402
  )
2195
- RETURNING notifications.message, notifications.serialization;`, [workflowID, topic])).rows;
2403
+ RETURNING notifications.message, notifications.serialization;`, [workflowID, topic, functionID])).rows;
2196
2404
  if (finalRecvRows.length > 0) {
2197
2405
  message = finalRecvRows[0].message;
2198
2406
  serialization = finalRecvRows[0].serialization;
@@ -4236,13 +4444,8 @@ class SystemDatabase {
4236
4444
  application_name
4237
4445
  ) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29)
4238
4446
  ON CONFLICT (workflow_uuid)
4239
- DO UPDATE SET
4240
- updated_at = (EXTRACT(EPOCH FROM now()) * 1000)::bigint,
4241
- executor_id = CASE
4242
- WHEN EXCLUDED.status != '${workflow_1.StatusString.ENQUEUED}' AND EXCLUDED.status != '${workflow_1.StatusString.DELAYED}'
4243
- THEN EXCLUDED.executor_id
4244
- ELSE workflow_status.executor_id
4245
- END
4447
+ -- A no-op update, so an existing row comes back unchanged for the caller to inspect.
4448
+ DO UPDATE SET owner_xid = workflow_status.owner_xid
4246
4449
  RETURNING status, name, class_name, config_name, queue_name, workflow_deadline_epoch_ms, executor_id, owner_xid, serialization`, [
4247
4450
  initStatus.workflowUUID,
4248
4451
  initStatus.status,
@@ -4274,7 +4477,6 @@ class SystemDatabase {
4274
4477
  initStatus.scheduleName ?? null,
4275
4478
  initStatus.debounceDeadlineEpochMS ?? null,
4276
4479
  initStatus.isDebounced ?? false,
4277
- // Absent from the conflict update: a re-enqueue must not re-own a claimed row.
4278
4480
  initStatus.applicationName ?? null,
4279
4481
  ]);
4280
4482
  if (rows.length === 0) {
@@ -4599,9 +4801,15 @@ exports.SystemDatabase = SystemDatabase;
4599
4801
  __decorate([
4600
4802
  dbRetry(),
4601
4803
  __metadata("design:type", Function),
4602
- __metadata("design:paramtypes", [Object, Object]),
4804
+ __metadata("design:paramtypes", [Object, Object, String]),
4603
4805
  __metadata("design:returntype", Promise)
4604
4806
  ], SystemDatabase.prototype, "initWorkflowStatusStandalone", null);
4807
+ __decorate([
4808
+ dbRetry(),
4809
+ __metadata("design:type", Function),
4810
+ __metadata("design:paramtypes", [Object, String, String, Number, Number, Number, String]),
4811
+ __metadata("design:returntype", Promise)
4812
+ ], SystemDatabase.prototype, "initChildWorkflowStatus", null);
4605
4813
  __decorate([
4606
4814
  dbRetry(),
4607
4815
  __metadata("design:type", Function),
@@ -4662,6 +4870,12 @@ __decorate([
4662
4870
  __metadata("design:paramtypes", [Object]),
4663
4871
  __metadata("design:returntype", Promise)
4664
4872
  ], SystemDatabase.prototype, "debounceDelayedWorkflowStandalone", null);
4873
+ __decorate([
4874
+ dbRetry(),
4875
+ __metadata("design:type", Function),
4876
+ __metadata("design:paramtypes", [String, Number, Object]),
4877
+ __metadata("design:returntype", Promise)
4878
+ ], SystemDatabase.prototype, "rewindWorkflow", null);
4665
4879
  __decorate([
4666
4880
  dbRetry()
4667
4881
  // A missing row normally means the workflow has not been inserted yet, so