@dbos-inc/dbos-sdk 2.1.5-preview.g539c9d794d → 2.1.9-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/dbos-config.schema.json +2 -11
  2. package/dist/src/context.d.ts +2 -0
  3. package/dist/src/context.d.ts.map +1 -1
  4. package/dist/src/context.js +16 -1
  5. package/dist/src/context.js.map +1 -1
  6. package/dist/src/dbos-executor.d.ts +9 -8
  7. package/dist/src/dbos-executor.d.ts.map +1 -1
  8. package/dist/src/dbos-executor.js +331 -29
  9. package/dist/src/dbos-executor.js.map +1 -1
  10. package/dist/src/dbos-runtime/cli.d.ts +1 -0
  11. package/dist/src/dbos-runtime/cli.d.ts.map +1 -1
  12. package/dist/src/dbos-runtime/cli.js +13 -2
  13. package/dist/src/dbos-runtime/cli.js.map +1 -1
  14. package/dist/src/dbos-runtime/config.d.ts +8 -7
  15. package/dist/src/dbos-runtime/config.d.ts.map +1 -1
  16. package/dist/src/dbos-runtime/config.js +26 -18
  17. package/dist/src/dbos-runtime/config.js.map +1 -1
  18. package/dist/src/dbos-runtime/db_connection.d.ts +10 -0
  19. package/dist/src/dbos-runtime/db_connection.d.ts.map +1 -0
  20. package/dist/src/dbos-runtime/db_connection.js +59 -0
  21. package/dist/src/dbos-runtime/db_connection.js.map +1 -0
  22. package/dist/src/dbos-runtime/db_wizard.d.ts.map +1 -1
  23. package/dist/src/dbos-runtime/db_wizard.js +10 -14
  24. package/dist/src/dbos-runtime/db_wizard.js.map +1 -1
  25. package/dist/src/dbos-runtime/migrate.d.ts.map +1 -1
  26. package/dist/src/dbos-runtime/migrate.js +2 -3
  27. package/dist/src/dbos-runtime/migrate.js.map +1 -1
  28. package/dist/src/dbos-runtime/reset.d.ts +4 -0
  29. package/dist/src/dbos-runtime/reset.d.ts.map +1 -0
  30. package/dist/src/dbos-runtime/reset.js +39 -0
  31. package/dist/src/dbos-runtime/reset.js.map +1 -0
  32. package/dist/src/dbos.d.ts +2 -0
  33. package/dist/src/dbos.d.ts.map +1 -1
  34. package/dist/src/dbos.js +50 -1
  35. package/dist/src/dbos.js.map +1 -1
  36. package/dist/src/debugger/debug_workflow.d.ts +1 -1
  37. package/dist/src/debugger/debug_workflow.d.ts.map +1 -1
  38. package/dist/src/debugger/debug_workflow.js +2 -2
  39. package/dist/src/debugger/debug_workflow.js.map +1 -1
  40. package/dist/src/error.d.ts +3 -0
  41. package/dist/src/error.d.ts.map +1 -1
  42. package/dist/src/error.js +10 -2
  43. package/dist/src/error.js.map +1 -1
  44. package/dist/src/eventreceiver.d.ts +2 -0
  45. package/dist/src/eventreceiver.d.ts.map +1 -1
  46. package/dist/src/httpServer/handler.js.map +1 -1
  47. package/dist/src/procedure.d.ts +3 -3
  48. package/dist/src/procedure.d.ts.map +1 -1
  49. package/dist/src/procedure.js +3 -1
  50. package/dist/src/procedure.js.map +1 -1
  51. package/dist/src/system_database.d.ts.map +1 -1
  52. package/dist/src/system_database.js +31 -4
  53. package/dist/src/system_database.js.map +1 -1
  54. package/dist/src/testing/testing_runtime.js.map +1 -1
  55. package/dist/src/utils.d.ts.map +1 -1
  56. package/dist/src/utils.js +1 -14
  57. package/dist/src/utils.js.map +1 -1
  58. package/dist/src/workflow.d.ts +1 -13
  59. package/dist/src/workflow.d.ts.map +1 -1
  60. package/dist/src/workflow.js +4 -322
  61. package/dist/src/workflow.js.map +1 -1
  62. package/dist/tsconfig.build.tsbuildinfo +1 -1
  63. package/package.json +1 -1
@@ -24,6 +24,7 @@ const debug_workflow_1 = require("./debugger/debug_workflow");
24
24
  const serialize_error_1 = require("serialize-error");
25
25
  const utils_1 = require("./utils");
26
26
  const node_path_1 = __importDefault(require("node:path"));
27
+ const procedure_1 = require("./procedure");
27
28
  const lodash_1 = require("lodash");
28
29
  const wfqueue_1 = require("./wfqueue");
29
30
  const debugpoint_1 = require("./debugpoint");
@@ -279,7 +280,6 @@ class DBOSExecutor {
279
280
  }
280
281
  this.logger.debug(`Loaded ${length} ORM entities`);
281
282
  }
282
- await ((0, user_database_1.createDBIfDoesNotExist)(this.config.poolConfig, this.logger));
283
283
  this.configureDbClient();
284
284
  if (!this.userDatabase) {
285
285
  this.logger.error("No user database configured!");
@@ -354,24 +354,6 @@ class DBOSExecutor {
354
354
  this.logger.error(`Unknown notice severity: ${msg.severity} - ${msg.message}`);
355
355
  }
356
356
  }
357
- async callProcedure(proc, args) {
358
- const client = await this.procedurePool.connect();
359
- const log = (msg) => this.#logNotice(msg);
360
- const procClassName = this.getProcedureClassName(proc);
361
- const plainProcName = `${procClassName}_${proc.name}_p`;
362
- const procName = this.config.appVersion
363
- ? `v${this.config.appVersion}_${plainProcName}`
364
- : plainProcName;
365
- const sql = `CALL "${procName}"(${args.map((_v, i) => `$${i + 1}`).join()});`;
366
- try {
367
- client.on('notice', log);
368
- return await client.query(sql, args).then(value => value.rows);
369
- }
370
- finally {
371
- client.off('notice', log);
372
- client.release();
373
- }
374
- }
375
357
  async destroy() {
376
358
  if (this.pendingWorkflowMap.size > 0) {
377
359
  this.logger.info("Waiting for pending workflows to finish.");
@@ -689,6 +671,128 @@ class DBOSExecutor {
689
671
  });
690
672
  return new workflow_1.InvokedHandle(this.systemDatabase, workflowPromise, workflowUUID, wf.name, callerUUID, callerFunctionID);
691
673
  }
674
+ /**
675
+ * Retrieve the transaction snapshot information of the current transaction
676
+ */
677
+ static async #retrieveSnapshot(query) {
678
+ const rows = await query("SELECT pg_current_snapshot()::text as txn_snapshot;", []);
679
+ return rows[0].txn_snapshot;
680
+ }
681
+ /**
682
+ * Check if an operation has already executed in a workflow.
683
+ * If it previously executed successfully, return its output.
684
+ * If it previously executed and threw an error, throw that error.
685
+ * Otherwise, return DBOSNull.
686
+ * Also return the transaction snapshot information of this current transaction.
687
+ */
688
+ async #checkExecution(query, workflowUUID, funcID) {
689
+ // Note: we read the current snapshot, not the recorded one!
690
+ const rows = await query("(SELECT output, error, txn_snapshot, true as recorded FROM dbos.transaction_outputs WHERE workflow_uuid=$1 AND function_id=$2 UNION ALL SELECT null as output, null as error, pg_current_snapshot()::text as txn_snapshot, false as recorded) ORDER BY recorded", [workflowUUID, funcID]);
691
+ if (rows.length === 0 || rows.length > 2) {
692
+ this.logger.error("Unexpected! This should never happen. Returned rows: " + rows.toString());
693
+ throw new error_1.DBOSError("This should never happen. Returned rows: " + rows.toString());
694
+ }
695
+ const res = {
696
+ output: exports.dbosNull,
697
+ txn_snapshot: ""
698
+ };
699
+ // recorded=false row will be first because we used ORDER BY.
700
+ res.txn_snapshot = rows[0].txn_snapshot;
701
+ if (rows.length === 2) {
702
+ if (utils_1.DBOSJSON.parse(rows[1].error) !== null) {
703
+ throw (0, serialize_error_1.deserializeError)(utils_1.DBOSJSON.parse(rows[1].error));
704
+ }
705
+ else {
706
+ res.output = utils_1.DBOSJSON.parse(rows[1].output);
707
+ }
708
+ }
709
+ return res;
710
+ }
711
+ /**
712
+ * Write a operation's output to the database.
713
+ */
714
+ static async #recordOutput(query, workflowUUID, funcID, txnSnapshot, output, isKeyConflict) {
715
+ try {
716
+ const serialOutput = utils_1.DBOSJSON.stringify(output);
717
+ const rows = await query("INSERT INTO dbos.transaction_outputs (workflow_uuid, function_id, output, txn_id, txn_snapshot, created_at) VALUES ($1, $2, $3, (select pg_current_xact_id_if_assigned()::text), $4, $5) RETURNING txn_id;", [workflowUUID, funcID, serialOutput, txnSnapshot, Date.now()]);
718
+ return rows[0].txn_id;
719
+ }
720
+ catch (error) {
721
+ if (isKeyConflict(error)) {
722
+ // Serialization and primary key conflict (Postgres).
723
+ throw new error_1.DBOSWorkflowConflictUUIDError(workflowUUID);
724
+ }
725
+ else {
726
+ throw error;
727
+ }
728
+ }
729
+ }
730
+ /**
731
+ * Record an error in an operation to the database.
732
+ */
733
+ static async #recordError(query, workflowUUID, funcID, txnSnapshot, err, isKeyConflict) {
734
+ try {
735
+ const serialErr = utils_1.DBOSJSON.stringify((0, serialize_error_1.serializeError)(err));
736
+ await query("INSERT INTO dbos.transaction_outputs (workflow_uuid, function_id, error, txn_id, txn_snapshot, created_at) VALUES ($1, $2, $3, null, $4, $5) RETURNING txn_id;", [workflowUUID, funcID, serialErr, txnSnapshot, Date.now()]);
737
+ }
738
+ catch (error) {
739
+ if (isKeyConflict(error)) {
740
+ // Serialization and primary key conflict (Postgres).
741
+ throw new error_1.DBOSWorkflowConflictUUIDError(workflowUUID);
742
+ }
743
+ else {
744
+ throw error;
745
+ }
746
+ }
747
+ }
748
+ /**
749
+ * Write all entries in the workflow result buffer to the database.
750
+ * If it encounters a primary key error, this indicates a concurrent execution with the same UUID, so throw an DBOSError.
751
+ */
752
+ async #flushResultBuffer(query, resultBuffer, workflowUUID, isKeyConflict) {
753
+ const funcIDs = Array.from(resultBuffer.keys());
754
+ if (funcIDs.length === 0) {
755
+ return;
756
+ }
757
+ funcIDs.sort();
758
+ try {
759
+ let sqlStmt = "INSERT INTO dbos.transaction_outputs (workflow_uuid, function_id, output, error, txn_id, txn_snapshot, created_at) VALUES ";
760
+ let paramCnt = 1;
761
+ const values = [];
762
+ for (const funcID of funcIDs) {
763
+ // Capture output and also transaction snapshot information.
764
+ // Initially, no txn_id because no queries executed.
765
+ const recorded = resultBuffer.get(funcID);
766
+ const output = recorded.output;
767
+ const txnSnapshot = recorded.txn_snapshot;
768
+ const createdAt = recorded.created_at;
769
+ if (paramCnt > 1) {
770
+ sqlStmt += ", ";
771
+ }
772
+ sqlStmt += `($${paramCnt++}, $${paramCnt++}, $${paramCnt++}, $${paramCnt++}, null, $${paramCnt++}, $${paramCnt++})`;
773
+ values.push(workflowUUID, funcID, utils_1.DBOSJSON.stringify(output), utils_1.DBOSJSON.stringify(null), txnSnapshot, createdAt);
774
+ }
775
+ this.logger.debug(sqlStmt);
776
+ await query(sqlStmt, values);
777
+ }
778
+ catch (error) {
779
+ if (isKeyConflict(error)) {
780
+ // Serialization and primary key conflict (Postgres).
781
+ throw new error_1.DBOSWorkflowConflictUUIDError(workflowUUID);
782
+ }
783
+ else {
784
+ throw error;
785
+ }
786
+ }
787
+ }
788
+ flushResultBuffer(client, resultBuffer, workflowUUID) {
789
+ const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
790
+ return this.#flushResultBuffer(func, resultBuffer, workflowUUID, (error) => this.userDatabase.isKeyConflictError(error));
791
+ }
792
+ #flushResultBufferProc(client, resultBuffer, workflowUUID) {
793
+ const func = (sql, args) => client.query(sql, args).then(v => v.rows);
794
+ return this.#flushResultBuffer(func, resultBuffer, workflowUUID, user_database_1.pgNodeIsKeyConflictError);
795
+ }
692
796
  async transaction(txn, params, ...args) {
693
797
  // Create a workflow and call transaction.
694
798
  const temp_workflow = async (ctxt, ...args) => {
@@ -729,7 +833,8 @@ class DBOSExecutor {
729
833
  // If the UUID is preset, it is possible this execution previously happened. Check, and return its original result if it did.
730
834
  // Note: It is possible to retrieve a generated ID from a workflow handle, run a concurrent execution, and cause trouble for yourself. We recommend against this.
731
835
  if (wfCtx.presetUUID) {
732
- const check = await wfCtx.checkTxExecution(client, funcId);
836
+ const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
837
+ const check = await this.#checkExecution(func, workflowUUID, funcId);
733
838
  txn_snapshot = check.txn_snapshot;
734
839
  if (check.output !== exports.dbosNull) {
735
840
  tCtxt.span.setAttribute("cached", true);
@@ -740,11 +845,12 @@ class DBOSExecutor {
740
845
  }
741
846
  else {
742
847
  // Collect snapshot information for read-only transactions and non-preset UUID transactions, if not already collected above
743
- txn_snapshot = await wfCtx.retrieveTxSnapshot(client);
848
+ const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
849
+ txn_snapshot = await DBOSExecutor.#retrieveSnapshot(func);
744
850
  }
745
851
  // For non-read-only transactions, flush the result buffer.
746
852
  if (!readOnly) {
747
- await wfCtx.flushResultBuffer(client);
853
+ await this.flushResultBuffer(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
748
854
  }
749
855
  // Execute the user's transaction.
750
856
  let cresult;
@@ -773,7 +879,8 @@ class DBOSExecutor {
773
879
  else {
774
880
  try {
775
881
  // Synchronously record the output of write transactions and obtain the transaction ID.
776
- const pg_txn_id = await wfCtx.recordOutputTx(client, funcId, txn_snapshot, result);
882
+ const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
883
+ const pg_txn_id = await DBOSExecutor.#recordOutput(func, wfCtx.workflowUUID, funcId, txn_snapshot, result, (error) => this.userDatabase.isKeyConflictError(error));
777
884
  tCtxt.span.setAttribute("pg_txn_id", pg_txn_id);
778
885
  wfCtx.resultBuffer.clear();
779
886
  }
@@ -808,8 +915,9 @@ class DBOSExecutor {
808
915
  // Record and throw other errors.
809
916
  const e = err;
810
917
  await this.userDatabase.transaction(async (client) => {
811
- await wfCtx.flushResultBuffer(client);
812
- await wfCtx.recordErrorTx(client, funcId, txn_snapshot, e);
918
+ await this.flushResultBuffer(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
919
+ const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
920
+ await DBOSExecutor.#recordError(func, wfCtx.workflowUUID, funcId, txn_snapshot, e, (error) => this.userDatabase.isKeyConflictError(error));
813
921
  }, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
814
922
  wfCtx.resultBuffer.clear();
815
923
  span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: e.message });
@@ -822,15 +930,209 @@ class DBOSExecutor {
822
930
  // Create a workflow and call procedure.
823
931
  const temp_workflow = async (ctxt, ...args) => {
824
932
  const ctxtImpl = ctxt;
825
- return await ctxtImpl.procedure(proc, ...args);
933
+ return this.callProcedureFunction(proc, ctxtImpl, ...args);
826
934
  };
827
- return (await this.workflow(temp_workflow, { ...params,
935
+ return await (await this.workflow(temp_workflow, {
936
+ ...params,
828
937
  tempWfType: TempWorkflowType.procedure,
829
938
  tempWfName: (0, decorators_1.getRegisteredMethodName)(proc),
830
939
  tempWfClass: (0, decorators_1.getRegisteredMethodClassName)(proc),
831
940
  }, ...args)).getResult();
832
941
  }
833
- async executeProcedure(func, config) {
942
+ async callProcedureFunction(proc, wfCtx, ...args) {
943
+ const procInfo = this.getProcedureInfo(proc);
944
+ if (procInfo === undefined) {
945
+ throw new error_1.DBOSNotRegisteredError(proc.name);
946
+ }
947
+ const executeLocally = procInfo.config.executeLocally ?? false;
948
+ const funcId = wfCtx.functionIDGetIncrement();
949
+ const span = this.tracer.startSpan(proc.name, {
950
+ operationUUID: wfCtx.workflowUUID,
951
+ operationType: exports.OperationType.PROCEDURE,
952
+ authenticatedUser: wfCtx.authenticatedUser,
953
+ assumedRole: wfCtx.assumedRole,
954
+ authenticatedRoles: wfCtx.authenticatedRoles,
955
+ readOnly: procInfo.config.readOnly ?? false,
956
+ isolationLevel: procInfo.config.isolationLevel,
957
+ executeLocally,
958
+ }, wfCtx.span);
959
+ try {
960
+ const result = executeLocally
961
+ ? await this.#callProcedureFunctionLocal(proc, args, wfCtx, span, procInfo, funcId)
962
+ : await this.#callProcedureFunctionRemote(proc, args, wfCtx, span, procInfo.config, funcId);
963
+ span.setStatus({ code: api_1.SpanStatusCode.OK });
964
+ return result;
965
+ }
966
+ catch (e) {
967
+ const { message } = e;
968
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message });
969
+ throw e;
970
+ }
971
+ finally {
972
+ this.tracer.endSpan(span);
973
+ }
974
+ }
975
+ async #callProcedureFunctionLocal(proc, args, wfCtx, span, procInfo, funcId) {
976
+ let retryWaitMillis = 1;
977
+ const backoffFactor = 1.5;
978
+ const maxRetryWaitMs = 2000; // Maximum wait 2 seconds.
979
+ const readOnly = procInfo.config.readOnly ?? false;
980
+ while (true) {
981
+ let txn_snapshot = "invalid";
982
+ const wrappedProcedure = async (client) => {
983
+ const ctxt = new procedure_1.StoredProcedureContextImpl(client, wfCtx, span, this.logger, funcId, proc.name);
984
+ if (wfCtx.presetUUID) {
985
+ const func = (sql, args) => this.procedurePool.query(sql, args).then(v => v.rows);
986
+ const check = await this.#checkExecution(func, wfCtx.workflowUUID, funcId);
987
+ txn_snapshot = check.txn_snapshot;
988
+ if (check.output !== exports.dbosNull) {
989
+ ctxt.span.setAttribute("cached", true);
990
+ ctxt.span.setStatus({ code: api_1.SpanStatusCode.OK });
991
+ this.tracer.endSpan(ctxt.span);
992
+ return check.output;
993
+ }
994
+ }
995
+ else {
996
+ // Collect snapshot information for read-only transactions and non-preset UUID transactions, if not already collected above
997
+ const func = (sql, args) => this.procedurePool.query(sql, args).then(v => v.rows);
998
+ txn_snapshot = await DBOSExecutor.#retrieveSnapshot(func);
999
+ }
1000
+ // For non-read-only transactions, flush the result buffer.
1001
+ if (!readOnly) {
1002
+ await this.#flushResultBufferProc(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
1003
+ }
1004
+ let cresult;
1005
+ if (procInfo.registration.passContext) {
1006
+ await (0, context_1.runWithStoredProcContext)(ctxt, async () => {
1007
+ cresult = await proc(ctxt, ...args);
1008
+ });
1009
+ }
1010
+ else {
1011
+ await (0, context_1.runWithStoredProcContext)(ctxt, async () => {
1012
+ const pf = proc;
1013
+ cresult = await pf(...args);
1014
+ });
1015
+ }
1016
+ const result = cresult;
1017
+ if (readOnly) {
1018
+ // Buffer the output of read-only transactions instead of synchronously writing it.
1019
+ const readOutput = {
1020
+ output: result,
1021
+ txn_snapshot: txn_snapshot,
1022
+ created_at: Date.now(),
1023
+ };
1024
+ wfCtx.resultBuffer.set(funcId, readOutput);
1025
+ }
1026
+ else {
1027
+ // Synchronously record the output of write transactions and obtain the transaction ID.
1028
+ const func = (sql, args) => client.query(sql, args).then(v => v.rows);
1029
+ const pg_txn_id = await DBOSExecutor.#recordOutput(func, wfCtx.workflowUUID, funcId, txn_snapshot, result, user_database_1.pgNodeIsKeyConflictError);
1030
+ // const pg_txn_id = await wfCtx.recordOutputProc<R>(client, funcId, txn_snapshot, result);
1031
+ ctxt.span.setAttribute("pg_txn_id", pg_txn_id);
1032
+ wfCtx.resultBuffer.clear();
1033
+ }
1034
+ return result;
1035
+ };
1036
+ try {
1037
+ const result = await this.invokeStoredProcFunction(wrappedProcedure, { isolationLevel: procInfo.config.isolationLevel });
1038
+ span.setStatus({ code: api_1.SpanStatusCode.OK });
1039
+ return result;
1040
+ }
1041
+ catch (err) {
1042
+ if (this.userDatabase.isRetriableTransactionError(err)) {
1043
+ // serialization_failure in PostgreSQL
1044
+ span.addEvent("TXN SERIALIZATION FAILURE", { "retryWaitMillis": retryWaitMillis }, performance.now());
1045
+ // Retry serialization failures.
1046
+ await (0, utils_1.sleepms)(retryWaitMillis);
1047
+ retryWaitMillis *= backoffFactor;
1048
+ retryWaitMillis = retryWaitMillis < maxRetryWaitMs ? retryWaitMillis : maxRetryWaitMs;
1049
+ continue;
1050
+ }
1051
+ // Record and throw other errors.
1052
+ const e = err;
1053
+ await this.invokeStoredProcFunction(async (client) => {
1054
+ await this.#flushResultBufferProc(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
1055
+ const func = (sql, args) => client.query(sql, args).then(v => v.rows);
1056
+ await DBOSExecutor.#recordError(func, wfCtx.workflowUUID, funcId, txn_snapshot, e, user_database_1.pgNodeIsKeyConflictError);
1057
+ }, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
1058
+ await this.userDatabase.transaction(async (client) => {
1059
+ await this.flushResultBuffer(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
1060
+ const func = (sql, args) => this.userDatabase.queryWithClient(client, sql, ...args);
1061
+ await DBOSExecutor.#recordError(func, wfCtx.workflowUUID, funcId, txn_snapshot, e, (error) => this.userDatabase.isKeyConflictError(error));
1062
+ }, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
1063
+ wfCtx.resultBuffer.clear();
1064
+ throw err;
1065
+ }
1066
+ }
1067
+ }
1068
+ async #callProcedureFunctionRemote(proc, args, wfCtx, span, config, funcId) {
1069
+ const readOnly = config.readOnly ?? false;
1070
+ const $jsonCtx = {
1071
+ request: wfCtx.request,
1072
+ authenticatedUser: wfCtx.authenticatedUser,
1073
+ authenticatedRoles: wfCtx.authenticatedRoles,
1074
+ assumedRole: wfCtx.assumedRole,
1075
+ };
1076
+ // Note, node-pg converts JS arrays to postgres array literals, so must call JSON.strigify on
1077
+ // args and bufferedResults before being passed to #invokeStoredProc
1078
+ const $args = [wfCtx.workflowUUID, funcId, wfCtx.presetUUID, $jsonCtx, null, JSON.stringify(args)];
1079
+ if (!readOnly) {
1080
+ // function_id, output, txn_snapshot, created_at
1081
+ const bufferedResults = new Array();
1082
+ for (const [functionID, { output, txn_snapshot, created_at }] of wfCtx.resultBuffer.entries()) {
1083
+ bufferedResults.push([functionID, output, txn_snapshot, created_at]);
1084
+ }
1085
+ // sort by function ID
1086
+ bufferedResults.sort((a, b) => a[0] - b[0]);
1087
+ $args.unshift(bufferedResults.length > 0 ? JSON.stringify(bufferedResults) : null);
1088
+ }
1089
+ const [{ return_value }] = await this.#invokeStoredProc(proc, $args);
1090
+ const { error, output, txn_snapshot, txn_id, created_at } = return_value;
1091
+ // buffered results are persisted in r/w stored procs, even if it returns an error
1092
+ if (!readOnly) {
1093
+ wfCtx.resultBuffer.clear();
1094
+ }
1095
+ // if the stored proc returns an error, deserialize and throw it.
1096
+ // stored proc saves the error in tx_output before returning
1097
+ if (error) {
1098
+ throw (0, serialize_error_1.deserializeError)(error);
1099
+ }
1100
+ // if txn_snapshot is provided, the output needs to be buffered
1101
+ if (readOnly && txn_snapshot) {
1102
+ wfCtx.resultBuffer.set(funcId, {
1103
+ output,
1104
+ txn_snapshot,
1105
+ created_at: created_at ?? Date.now(),
1106
+ });
1107
+ }
1108
+ if (!readOnly) {
1109
+ wfCtx.resultBuffer.clear();
1110
+ }
1111
+ if (txn_id) {
1112
+ span.setAttribute("pg_txn_id", txn_id);
1113
+ }
1114
+ span.setStatus({ code: api_1.SpanStatusCode.OK });
1115
+ return output;
1116
+ }
1117
+ async #invokeStoredProc(proc, args) {
1118
+ const client = await this.procedurePool.connect();
1119
+ const log = (msg) => this.#logNotice(msg);
1120
+ const procClassName = this.getProcedureClassName(proc);
1121
+ const plainProcName = `${procClassName}_${proc.name}_p`;
1122
+ const procName = this.config.appVersion
1123
+ ? `v${this.config.appVersion}_${plainProcName}`
1124
+ : plainProcName;
1125
+ const sql = `CALL "${procName}"(${args.map((_v, i) => `$${i + 1}`).join()});`;
1126
+ try {
1127
+ client.on('notice', log);
1128
+ return await client.query(sql, args).then(value => value.rows);
1129
+ }
1130
+ finally {
1131
+ client.off('notice', log);
1132
+ client.release();
1133
+ }
1134
+ }
1135
+ async invokeStoredProcFunction(func, config) {
834
1136
  const client = await this.procedurePool.connect();
835
1137
  try {
836
1138
  const readOnly = config.readOnly ?? false;
@@ -889,7 +1191,7 @@ class DBOSExecutor {
889
1191
  }, wfCtx.span);
890
1192
  const ctxt = new step_1.StepContextImpl(wfCtx, funcID, span, this.logger, commInfo.config, stepFn.name);
891
1193
  await this.userDatabase.transaction(async (client) => {
892
- await wfCtx.flushResultBuffer(client);
1194
+ await this.flushResultBuffer(client, wfCtx.resultBuffer, wfCtx.workflowUUID);
893
1195
  }, { isolationLevel: transaction_1.IsolationLevel.ReadCommitted });
894
1196
  wfCtx.resultBuffer.clear();
895
1197
  // Check if this execution previously happened, returning its original result if it did.