@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/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -75
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +75 -105
- package/package.json +5 -6
- package/tests/config.test.ts +1 -1
- package/tests/datasource.test.ts +58 -179
- package/tests/test-helpers.ts +2 -3
package/index.ts
CHANGED
|
@@ -25,56 +25,41 @@ export { NodePostgresTransactionOptions };
|
|
|
25
25
|
|
|
26
26
|
const asyncLocalCtx = new AsyncLocalStorage<NodePostgresDataSourceContext>();
|
|
27
27
|
|
|
28
|
-
class
|
|
28
|
+
class NodePGDSTH implements DataSourceTransactionHandler {
|
|
29
|
+
readonly name: string;
|
|
29
30
|
readonly dsType = 'NodePostgresDataSource';
|
|
30
|
-
#
|
|
31
|
+
readonly #pool: Pool;
|
|
31
32
|
|
|
32
|
-
constructor(
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
this.#poolField = undefined;
|
|
46
|
-
await pool?.end();
|
|
38
|
+
initialize(): Promise<void> {
|
|
39
|
+
return Promise.resolve();
|
|
47
40
|
}
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
59
|
-
): Promise<{ output: string | null } |
|
|
60
|
-
|
|
61
|
-
|
|
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,
|
|
54
|
+
[workflowID, functionNum],
|
|
66
55
|
);
|
|
67
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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;
|
|
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>(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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('
|
|
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
|
-
|
|
202
|
+
readonly name: string;
|
|
203
|
+
#provider: NodePGDSTH;
|
|
232
204
|
|
|
233
|
-
constructor(
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
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:
|
|
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,
|
|
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.
|
|
4
|
-
"description": "
|
|
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/
|
|
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"
|
package/tests/config.test.ts
CHANGED
|
@@ -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);
|
|
13
13
|
await ensureDB(client, config.database);
|
|
14
14
|
} finally {
|
|
15
15
|
await client.end();
|