@dbos-inc/node-pg-datasource 3.0.11-preview.gc9233b8190 → 3.0.13-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.
package/index.ts CHANGED
@@ -25,41 +25,56 @@ export { NodePostgresTransactionOptions };
25
25
 
26
26
  const asyncLocalCtx = new AsyncLocalStorage<NodePostgresDataSourceContext>();
27
27
 
28
- class NodePGDSTH implements DataSourceTransactionHandler {
29
- readonly name: string;
28
+ class NodePostgresTransactionHandler implements DataSourceTransactionHandler {
30
29
  readonly dsType = 'NodePostgresDataSource';
31
- readonly #pool: Pool;
30
+ #poolField: Pool | undefined;
32
31
 
33
- constructor(name: string, config: PoolConfig) {
34
- this.name = name;
35
- this.#pool = new Pool(config);
32
+ constructor(
33
+ readonly name: string,
34
+ private readonly config: PoolConfig,
35
+ ) {}
36
+
37
+ async initialize(): Promise<void> {
38
+ const pool = this.#poolField;
39
+ this.#poolField = new Pool(this.config);
40
+ await pool?.end();
36
41
  }
37
42
 
38
- initialize(): Promise<void> {
39
- return Promise.resolve();
43
+ async destroy(): Promise<void> {
44
+ const pool = this.#poolField;
45
+ this.#poolField = undefined;
46
+ await pool?.end();
40
47
  }
41
48
 
42
- destroy(): Promise<void> {
43
- return this.#pool.end();
49
+ get #pool(): Pool {
50
+ if (!this.#poolField) {
51
+ throw new Error(`DataSource ${this.name} is not initialized.`);
52
+ }
53
+ return this.#poolField;
44
54
  }
45
55
 
46
- static async #checkExecution(
47
- client: ClientBase,
56
+ async #checkExecution(
48
57
  workflowID: string,
49
- functionNum: number,
50
- ): Promise<{ output: string | null } | undefined> {
51
- const { rows } = await client.query<{ output: string }>(
52
- /*sql*/ `SELECT output FROM dbos.transaction_completion
58
+ stepID: number,
59
+ ): Promise<{ output: string | null } | { error: string } | undefined> {
60
+ type Result = { output: string | null; error: string | null };
61
+ const { rows } = await this.#pool.query<Result>(
62
+ /*sql*/
63
+ `SELECT output, error FROM dbos.transaction_completion
53
64
  WHERE workflow_id = $1 AND function_num = $2`,
54
- [workflowID, functionNum],
65
+ [workflowID, stepID],
55
66
  );
56
- return rows.length > 0 ? { output: rows[0].output } : undefined;
67
+ if (rows.length === 0) {
68
+ return undefined;
69
+ }
70
+ const { output, error } = rows[0];
71
+ return error !== null ? { error } : { output };
57
72
  }
58
73
 
59
74
  static async #recordOutput(
60
75
  client: ClientBase,
61
76
  workflowID: string,
62
- functionNum: number,
77
+ stepID: number,
63
78
  output: string | null,
64
79
  ): Promise<void> {
65
80
  try {
@@ -67,7 +82,24 @@ class NodePGDSTH implements DataSourceTransactionHandler {
67
82
  /*sql*/
68
83
  `INSERT INTO dbos.transaction_completion (workflow_id, function_num, output)
69
84
  VALUES ($1, $2, $3)`,
70
- [workflowID, functionNum, output],
85
+ [workflowID, stepID, output],
86
+ );
87
+ } catch (error) {
88
+ if (isPGKeyConflictError(error)) {
89
+ throw new DBOSWorkflowConflictError(workflowID);
90
+ } else {
91
+ throw error;
92
+ }
93
+ }
94
+ }
95
+
96
+ async #recordError(workflowID: string, stepID: number, error: string): Promise<void> {
97
+ try {
98
+ await this.#pool.query(
99
+ /*sql*/
100
+ `INSERT INTO dbos.transaction_completion (workflow_id, function_num, error)
101
+ VALUES ($1, $2, $3)`,
102
+ [workflowID, stepID, error],
71
103
  );
72
104
  } catch (error) {
73
105
  if (isPGKeyConflictError(error)) {
@@ -107,67 +139,64 @@ class NodePGDSTH implements DataSourceTransactionHandler {
107
139
  ...args: Args
108
140
  ): Promise<Return> {
109
141
  const workflowID = DBOS.workflowID;
110
- if (workflowID === undefined) {
111
- throw new Error('Workflow ID is not set.');
112
- }
113
- const functionNum = DBOS.stepID;
114
- if (functionNum === undefined) {
115
- throw new Error('Function Number is not set.');
142
+ const stepID = DBOS.stepID;
143
+ if (workflowID !== undefined && stepID === undefined) {
144
+ throw new Error('DBOS.stepID is undefined inside a workflow.');
116
145
  }
117
146
 
118
147
  const readOnly = config?.readOnly ?? false;
148
+ const saveResults = !readOnly && workflowID !== undefined;
149
+
150
+ // Retry loop if appropriate
119
151
  let retryWaitMS = 1;
120
152
  const backoffFactor = 1.5;
121
- const maxRetryWaitMS = 2000;
153
+ const maxRetryWaitMS = 2000; // Maximum wait 2 seconds.
122
154
 
123
155
  while (true) {
156
+ // Check to see if this tx has already been executed
157
+ const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID!) : undefined;
158
+ if (previousResult) {
159
+ DBOS.span?.setAttribute('cached', true);
160
+
161
+ if ('error' in previousResult) {
162
+ throw SuperJSON.parse(previousResult.error);
163
+ }
164
+
165
+ return (previousResult.output ? SuperJSON.parse(previousResult.output) : null) as Return;
166
+ }
167
+
124
168
  try {
125
- const result = await this.#transaction<Return>(
126
- async (client) => {
127
- // Check to see if this tx has already been executed
128
- const previousResult =
129
- readOnly || !workflowID ? undefined : await NodePGDSTH.#checkExecution(client, workflowID, functionNum);
130
- if (previousResult) {
131
- return (previousResult.output ? SuperJSON.parse(previousResult.output) : null) as Return;
132
- }
133
-
134
- // execute user's transaction function
135
- const result = await asyncLocalCtx.run({ client }, async () => {
136
- return (await func.call(target, ...args)) as Return;
137
- });
138
-
139
- // save the output of read/write transactions
140
- if (!readOnly && workflowID) {
141
- await NodePGDSTH.#recordOutput(client, workflowID, functionNum, SuperJSON.stringify(result));
142
-
143
- // Note, existing code wraps #recordOutput call in a try/catch block that
144
- // converts DB error with code 25P02 to DBOSFailedSqlTransactionError.
145
- // However, existing code doesn't make any logic decisions based on that error type.
146
- // DBOSFailedSqlTransactionError does stored WF ID and function name, so I assume that info is logged out somewhere
147
- }
148
-
149
- return result;
150
- },
151
- { isolationLevel: config?.isolationLevel, readOnly: config?.readOnly },
152
- );
153
- // TODO: span.setStatus({ code: SpanStatusCode.OK });
154
- // TODO: this.tracer.endSpan(span);
169
+ const result = await this.#transaction<Return>(async (client) => {
170
+ // execute user's transaction function
171
+ const result = await asyncLocalCtx.run({ client }, async () => {
172
+ return (await func.call(target, ...args)) as Return;
173
+ });
174
+
175
+ // save the output of read/write transactions
176
+ if (saveResults) {
177
+ await NodePostgresTransactionHandler.#recordOutput(
178
+ client,
179
+ workflowID,
180
+ stepID!,
181
+ SuperJSON.stringify(result),
182
+ );
183
+ }
184
+
185
+ return result;
186
+ }, config);
155
187
 
156
188
  return result;
157
189
  } catch (error) {
158
190
  if (isPGRetriableTransactionError(error)) {
159
- // TODO: span.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMillis }, performance.now());
191
+ DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
160
192
  await new Promise((resolve) => setTimeout(resolve, retryWaitMS));
161
193
  retryWaitMS = Math.min(retryWaitMS * backoffFactor, maxRetryWaitMS);
162
194
  continue;
163
195
  } else {
164
- // TODO: span.setStatus({ code: SpanStatusCode.ERROR, message: e.message });
165
- // TODO: this.tracer.endSpan(span);
166
-
167
- // TODO: currently, we are *not* recording errors in the txOutput table.
168
- // For normal execution, this is fine because we also store tx step results (output and error) in the sysdb operation output table.
169
- // However, I'm concerned that we have a dueling execution hole where one tx fails while another succeeds.
170
- // This implies that we can end up in a situation where the step output records an error but the txOutput table records success.
196
+ if (saveResults) {
197
+ const message = SuperJSON.stringify(error);
198
+ await this.#recordError(workflowID, stepID!, message);
199
+ }
171
200
 
172
201
  throw error;
173
202
  }
@@ -183,7 +212,7 @@ export class NodePostgresDataSource implements DBOSDataSource<NodePostgresTransa
183
212
  }
184
213
  const ctx = asyncLocalCtx.getStore();
185
214
  if (!ctx) {
186
- throw new Error('No async local context found.');
215
+ throw new Error('invalid use of NodePostgresDataSource.client outside of a DBOS transaction.');
187
216
  }
188
217
  return ctx.client;
189
218
  }
@@ -199,12 +228,13 @@ export class NodePostgresDataSource implements DBOSDataSource<NodePostgresTransa
199
228
  }
200
229
  }
201
230
 
202
- readonly name: string;
203
- #provider: NodePGDSTH;
231
+ #provider: NodePostgresTransactionHandler;
204
232
 
205
- constructor(name: string, config: PoolConfig) {
206
- this.name = name;
207
- this.#provider = new NodePGDSTH(name, config);
233
+ constructor(
234
+ readonly name: string,
235
+ config: PoolConfig,
236
+ ) {
237
+ this.#provider = new NodePostgresTransactionHandler(name, config);
208
238
  registerDataSource(this.#provider);
209
239
  }
210
240
 
@@ -214,10 +244,10 @@ export class NodePostgresDataSource implements DBOSDataSource<NodePostgresTransa
214
244
 
215
245
  registerTransaction<This, Args extends unknown[], Return>(
216
246
  func: (this: This, ...args: Args) => Promise<Return>,
217
- name: string,
218
247
  config?: NodePostgresTransactionOptions,
248
+ name?: string,
219
249
  ): (this: This, ...args: Args) => Promise<Return> {
220
- return registerTransaction(this.name, func, { name }, config);
250
+ return registerTransaction(this.name, func, { name: name ?? func.name }, config);
221
251
  }
222
252
 
223
253
  transaction(config?: NodePostgresTransactionOptions) {
@@ -225,14 +255,14 @@ export class NodePostgresDataSource implements DBOSDataSource<NodePostgresTransa
225
255
  const ds = this;
226
256
  return function decorator<This, Args extends unknown[], Return>(
227
257
  _target: object,
228
- propertyKey: string,
258
+ propertyKey: PropertyKey,
229
259
  descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>,
230
260
  ) {
231
261
  if (!descriptor.value) {
232
262
  throw Error('Use of decorator when original method is undefined');
233
263
  }
234
264
 
235
- descriptor.value = ds.registerTransaction(descriptor.value, propertyKey.toString(), config);
265
+ descriptor.value = ds.registerTransaction(descriptor.value, config, String(propertyKey));
236
266
 
237
267
  return descriptor;
238
268
  };
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@dbos-inc/node-pg-datasource",
3
- "version": "3.0.11-preview.gc9233b8190",
4
- "description": "",
3
+ "version": "3.0.13-preview",
4
+ "description": "DBOS DataSource library for Node-Postgres database client",
5
5
  "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "homepage": "https://docs.dbos.dev/",
6
9
  "repository": {
7
10
  "type": "git",
8
11
  "url": "https://github.com/dbos-inc/dbos-transact-ts",
9
- "directory": "packages/knex-datasource"
12
+ "directory": "packages/nodepg-datasource"
10
13
  },
11
- "homepage": "https://docs.dbos.dev/",
12
- "main": "index.js",
13
14
  "scripts": {
14
15
  "build": "tsc --project tsconfig.json",
15
16
  "test": "jest --detectOpenHandles"
@@ -9,7 +9,7 @@ describe('NodePostgresDataSource.configure', () => {
9
9
  const client = new Client({ ...config, database: 'postgres' });
10
10
  try {
11
11
  await client.connect();
12
- await dropDB(client, config.database);
12
+ await dropDB(client, config.database, true);
13
13
  await ensureDB(client, config.database);
14
14
  } finally {
15
15
  await client.end();