@dbos-inc/node-pg-datasource 3.0.10-preview → 3.0.11-preview.gc9233b8190

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,56 +25,41 @@ export { NodePostgresTransactionOptions };
25
25
 
26
26
  const asyncLocalCtx = new AsyncLocalStorage<NodePostgresDataSourceContext>();
27
27
 
28
- class NodePostgresTransactionHandler implements DataSourceTransactionHandler {
28
+ class NodePGDSTH implements DataSourceTransactionHandler {
29
+ readonly name: string;
29
30
  readonly dsType = 'NodePostgresDataSource';
30
- #poolField: Pool | undefined;
31
+ readonly #pool: Pool;
31
32
 
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();
33
+ constructor(name: string, config: PoolConfig) {
34
+ this.name = name;
35
+ this.#pool = new Pool(config);
41
36
  }
42
37
 
43
- async destroy(): Promise<void> {
44
- const pool = this.#poolField;
45
- this.#poolField = undefined;
46
- await pool?.end();
38
+ initialize(): Promise<void> {
39
+ return Promise.resolve();
47
40
  }
48
41
 
49
- get #pool(): Pool {
50
- if (!this.#poolField) {
51
- throw new Error(`DataSource ${this.name} is not initialized.`);
52
- }
53
- return this.#poolField;
42
+ destroy(): Promise<void> {
43
+ return this.#pool.end();
54
44
  }
55
45
 
56
- async #checkExecution(
46
+ static async #checkExecution(
47
+ client: ClientBase,
57
48
  workflowID: string,
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
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
64
53
  WHERE workflow_id = $1 AND function_num = $2`,
65
- [workflowID, stepID],
54
+ [workflowID, functionNum],
66
55
  );
67
- if (rows.length === 0) {
68
- return undefined;
69
- }
70
- const { output, error } = rows[0];
71
- return error !== null ? { error } : { output };
56
+ return rows.length > 0 ? { output: rows[0].output } : undefined;
72
57
  }
73
58
 
74
59
  static async #recordOutput(
75
60
  client: ClientBase,
76
61
  workflowID: string,
77
- stepID: number,
62
+ functionNum: number,
78
63
  output: string | null,
79
64
  ): Promise<void> {
80
65
  try {
@@ -82,24 +67,7 @@ class NodePostgresTransactionHandler implements DataSourceTransactionHandler {
82
67
  /*sql*/
83
68
  `INSERT INTO dbos.transaction_completion (workflow_id, function_num, output)
84
69
  VALUES ($1, $2, $3)`,
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],
70
+ [workflowID, functionNum, output],
103
71
  );
104
72
  } catch (error) {
105
73
  if (isPGKeyConflictError(error)) {
@@ -139,64 +107,67 @@ class NodePostgresTransactionHandler implements DataSourceTransactionHandler {
139
107
  ...args: Args
140
108
  ): Promise<Return> {
141
109
  const workflowID = DBOS.workflowID;
142
- const stepID = DBOS.stepID;
143
- if (workflowID !== undefined && stepID === undefined) {
144
- throw new Error('DBOS.stepID is undefined inside a workflow.');
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.');
145
116
  }
146
117
 
147
118
  const readOnly = config?.readOnly ?? false;
148
- const saveResults = !readOnly && workflowID !== undefined;
149
-
150
- // Retry loop if appropriate
151
119
  let retryWaitMS = 1;
152
120
  const backoffFactor = 1.5;
153
- const maxRetryWaitMS = 2000; // Maximum wait 2 seconds.
121
+ const maxRetryWaitMS = 2000;
154
122
 
155
123
  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
-
168
124
  try {
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);
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);
187
155
 
188
156
  return result;
189
157
  } catch (error) {
190
158
  if (isPGRetriableTransactionError(error)) {
191
- DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
159
+ // TODO: span.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMillis }, performance.now());
192
160
  await new Promise((resolve) => setTimeout(resolve, retryWaitMS));
193
161
  retryWaitMS = Math.min(retryWaitMS * backoffFactor, maxRetryWaitMS);
194
162
  continue;
195
163
  } else {
196
- if (saveResults) {
197
- const message = SuperJSON.stringify(error);
198
- await this.#recordError(workflowID, stepID!, message);
199
- }
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.
200
171
 
201
172
  throw error;
202
173
  }
@@ -212,7 +183,7 @@ export class NodePostgresDataSource implements DBOSDataSource<NodePostgresTransa
212
183
  }
213
184
  const ctx = asyncLocalCtx.getStore();
214
185
  if (!ctx) {
215
- throw new Error('invalid use of NodePostgresDataSource.client outside of a DBOS transaction.');
186
+ throw new Error('No async local context found.');
216
187
  }
217
188
  return ctx.client;
218
189
  }
@@ -228,13 +199,12 @@ export class NodePostgresDataSource implements DBOSDataSource<NodePostgresTransa
228
199
  }
229
200
  }
230
201
 
231
- #provider: NodePostgresTransactionHandler;
202
+ readonly name: string;
203
+ #provider: NodePGDSTH;
232
204
 
233
- constructor(
234
- readonly name: string,
235
- config: PoolConfig,
236
- ) {
237
- this.#provider = new NodePostgresTransactionHandler(name, config);
205
+ constructor(name: string, config: PoolConfig) {
206
+ this.name = name;
207
+ this.#provider = new NodePGDSTH(name, config);
238
208
  registerDataSource(this.#provider);
239
209
  }
240
210
 
@@ -244,10 +214,10 @@ export class NodePostgresDataSource implements DBOSDataSource<NodePostgresTransa
244
214
 
245
215
  registerTransaction<This, Args extends unknown[], Return>(
246
216
  func: (this: This, ...args: Args) => Promise<Return>,
217
+ name: string,
247
218
  config?: NodePostgresTransactionOptions,
248
- name?: string,
249
219
  ): (this: This, ...args: Args) => Promise<Return> {
250
- return registerTransaction(this.name, func, { name: name ?? func.name }, config);
220
+ return registerTransaction(this.name, func, { name }, config);
251
221
  }
252
222
 
253
223
  transaction(config?: NodePostgresTransactionOptions) {
@@ -255,14 +225,14 @@ export class NodePostgresDataSource implements DBOSDataSource<NodePostgresTransa
255
225
  const ds = this;
256
226
  return function decorator<This, Args extends unknown[], Return>(
257
227
  _target: object,
258
- propertyKey: PropertyKey,
228
+ propertyKey: string,
259
229
  descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>,
260
230
  ) {
261
231
  if (!descriptor.value) {
262
232
  throw Error('Use of decorator when original method is undefined');
263
233
  }
264
234
 
265
- descriptor.value = ds.registerTransaction(descriptor.value, config, String(propertyKey));
235
+ descriptor.value = ds.registerTransaction(descriptor.value, propertyKey.toString(), config);
266
236
 
267
237
  return descriptor;
268
238
  };
package/package.json CHANGED
@@ -1,16 +1,15 @@
1
1
  {
2
2
  "name": "@dbos-inc/node-pg-datasource",
3
- "version": "3.0.10-preview",
4
- "description": "DBOS DataSource library for Node-Postgres database client",
3
+ "version": "3.0.11-preview.gc9233b8190",
4
+ "description": "",
5
5
  "license": "MIT",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "homepage": "https://docs.dbos.dev/",
9
6
  "repository": {
10
7
  "type": "git",
11
8
  "url": "https://github.com/dbos-inc/dbos-transact-ts",
12
- "directory": "packages/nodepg-datasource"
9
+ "directory": "packages/knex-datasource"
13
10
  },
11
+ "homepage": "https://docs.dbos.dev/",
12
+ "main": "index.js",
14
13
  "scripts": {
15
14
  "build": "tsc --project tsconfig.json",
16
15
  "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, true);
12
+ await dropDB(client, config.database);
13
13
  await ensureDB(client, config.database);
14
14
  } finally {
15
15
  await client.end();