@dbos-inc/dbos-sdk 2.1.5-preview.g539c9d794d → 2.1.9-preview

Sign up to get free protection for your applications and to get access to all the features.
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.