@duraflows/kysely 5.1.0 → 5.2.1
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/README.md +32 -14
- package/dist/cjs/index.js +20 -5
- package/dist/cjs/kysely-transaction-context.d.ts +38 -2
- package/dist/cjs/kysely-transaction-context.js +81 -14
- package/dist/cjs/kysely-transaction-runner.d.ts +2 -1
- package/dist/cjs/kysely-transaction-runner.js +9 -7
- package/dist/index.js +21 -6
- package/dist/kysely-transaction-context.d.ts +38 -2
- package/dist/kysely-transaction-context.js +78 -13
- package/dist/kysely-transaction-runner.d.ts +2 -1
- package/dist/kysely-transaction-runner.js +10 -8
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -49,27 +49,42 @@ The primary use case for this package is running Duraflows workflow transitions
|
|
|
49
49
|
```ts
|
|
50
50
|
import { KyselyTransactionContext } from "@duraflows/kysely";
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
workflowInstanceUuid: instanceUuid,
|
|
61
|
-
eventName: "PaymentReceived",
|
|
62
|
-
});
|
|
52
|
+
await KyselyTransactionContext.transaction(db, async (trx) => {
|
|
53
|
+
// Your own writes -- uses trx
|
|
54
|
+
await trx.insertInto("orders").values({ id: "ORD-1", status: "paid" }).execute();
|
|
55
|
+
|
|
56
|
+
// Duraflows writes -- also uses trx (same transaction)
|
|
57
|
+
await runtime.triggerEvent({
|
|
58
|
+
workflowInstanceUuid: instanceUuid,
|
|
59
|
+
eventName: "PaymentReceived",
|
|
63
60
|
});
|
|
64
61
|
});
|
|
65
|
-
// Both commit or both
|
|
62
|
+
// Both commit or both roll back. Observers fire after COMMIT, never after a rollback.
|
|
66
63
|
```
|
|
67
64
|
|
|
65
|
+
Each duraflows call inside runs in its own savepoint. If one fails, its writes are rolled back, and your transaction can carry on if you catch the error. Calling `transaction()` inside one that is already active joins it as a savepoint instead of opening a second transaction, so composed service methods stay atomic.
|
|
66
|
+
|
|
67
|
+
"Never after a rollback" has one caveat: if a statement failed and you swallowed its error, PostgreSQL silently turns kysely's `COMMIT` into a rollback, and kysely can't see that. Observers then fire for writes that were never persisted. Don't swallow SQL errors inside a transaction: let them propagate, or run the statement in a nested `KyselyTransactionContext.transaction()` and catch its rejection, which rolls back only that savepoint.
|
|
68
|
+
|
|
69
|
+
The runner's `lockTimeoutMs` / `statementTimeoutMs` don't apply inside `transaction()` or a seeded `run()`: you own those transaction settings, and nested duraflows calls run under them.
|
|
70
|
+
|
|
71
|
+
If you already open the transaction yourself, you can still seed the context:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
await db.transaction().execute((trx) =>
|
|
75
|
+
KyselyTransactionContext.run(db, trx, async () => {
|
|
76
|
+
await runtime.triggerEvent({ workflowInstanceUuid: instanceUuid, eventName: "PaymentReceived" });
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Observers then fire when the seeded callback resolves, which is **before** kysely commits, and duraflows calls they make join your still-open transaction. Prefer `transaction()` when observers must never see a rolled-back write. Don't run duraflows calls concurrently (`Promise.all`) on one transaction.
|
|
82
|
+
|
|
68
83
|
### How It Works
|
|
69
84
|
|
|
70
85
|
`KyselyTransactionContext` uses Node.js `AsyncLocalStorage` to propagate the active Kysely transaction through the async call chain. When Duraflows' internal `WorkflowTransactionRunner.runInTransaction()` is called, it checks for an existing transaction in the context:
|
|
71
86
|
|
|
72
|
-
- **Found:**
|
|
87
|
+
- **Found:** Runs the call in a savepoint on it (`SAVEPOINT` … `RELEASE`, or `ROLLBACK TO` on failure)
|
|
73
88
|
- **Not found:** Starts a new Kysely transaction and seeds the context
|
|
74
89
|
|
|
75
90
|
This re-entrancy contract means your application code controls the transaction boundary.
|
|
@@ -127,7 +142,9 @@ Both are applied transaction-locally via `set_config(name, value, true)` — the
|
|
|
127
142
|
|
|
128
143
|
### `kyselyWorkflowProvidersFromTransaction(trx): WorkflowPersistenceProvider`
|
|
129
144
|
|
|
130
|
-
Convenience factory for short-lived runtimes pre-bound to an existing transaction.
|
|
145
|
+
Convenience factory for short-lived runtimes pre-bound to an existing transaction. Each workflow call runs in a savepoint on `trx`, so a failed call leaves no partial writes behind even if you catch its error. Observers fire when the call resolves, before your transaction commits, and duraflows calls they make join `trx`. For strictly post-commit observers, use `KyselyTransactionContext.transaction(db, …)` with the long-lived providers instead.
|
|
146
|
+
|
|
147
|
+
Don't mix these providers with the long-lived `kyselyWorkflowProviders(db)` (or `KyselyTransactionContext.transaction(db, …)`) inside one transaction. The two track their active transaction separately, one per `trx` and the other per `db`, so a call through one doesn't see a transaction opened through the other. It starts a second transaction on another connection instead, which can wait on row locks the first one holds. Use one style per transaction.
|
|
131
148
|
|
|
132
149
|
### `KyselyTransactionContext`
|
|
133
150
|
|
|
@@ -135,6 +152,7 @@ The context is scoped per Kysely instance:
|
|
|
135
152
|
|
|
136
153
|
- `getTransaction(db)` -- Returns the active `Transaction<WorkflowDatabase>` for the given `db` instance, or `undefined`
|
|
137
154
|
- `run(db, trx, callback)` -- Executes `callback` with `trx` as the active transaction context for the given `db` instance
|
|
155
|
+
- `transaction(db, callback)` -- Runs `callback(trx)` in a new transaction on `db` with `trx` active for workflow calls; observers fire after it commits. Inside an already-active transaction for `db`, it joins that transaction as a savepoint instead
|
|
138
156
|
|
|
139
157
|
## Documentation
|
|
140
158
|
|
package/dist/cjs/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.KyselyWorkflowDefinitionStore = exports.KyselyWorkflowHistoryStore = exports.KyselyWorkflowInstanceStore = exports.KyselyTransactionRunner = exports.KyselyTransactionContext = void 0;
|
|
4
4
|
exports.kyselyWorkflowProviders = kyselyWorkflowProviders;
|
|
5
5
|
exports.kyselyWorkflowProvidersFromTransaction = kyselyWorkflowProvidersFromTransaction;
|
|
6
|
+
const core_1 = require("@duraflows/core");
|
|
6
7
|
const kysely_instance_store_js_1 = require("./kysely-instance-store.js");
|
|
7
8
|
const kysely_history_store_js_1 = require("./kysely-history-store.js");
|
|
8
9
|
const kysely_definition_store_js_1 = require("./kysely-definition-store.js");
|
|
@@ -41,14 +42,28 @@ function kyselyWorkflowProviders(db, options = {}) {
|
|
|
41
42
|
*/
|
|
42
43
|
function kyselyWorkflowProvidersFromTransaction(trx) {
|
|
43
44
|
const narrowed = trx;
|
|
45
|
+
const boundTrx = trx;
|
|
44
46
|
const transactionRunner = {
|
|
45
47
|
async runInTransaction(callback) {
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// Scoped to the bound trx; an unrelated ambient transaction from another
|
|
49
|
+
// provider must never supersede it.
|
|
50
|
+
const scope = kysely_transaction_context_js_1.kyselyTransactionScopes.current(narrowed);
|
|
51
|
+
if (scope) {
|
|
52
|
+
return (0, kysely_transaction_context_js_1.runInKyselySavepoint)(narrowed, scope, callback);
|
|
50
53
|
}
|
|
51
|
-
|
|
54
|
+
// The caller owns `trx`, so even this outermost call is nested in their
|
|
55
|
+
// transaction: run it in a savepoint so a failure leaves no partial
|
|
56
|
+
// writes behind, and deliver its observers once it resolves. `trx` is
|
|
57
|
+
// still open then, so the observers run inside the root scope: a
|
|
58
|
+
// duraflows call they make joins `trx` as a savepoint, and the
|
|
59
|
+
// callbacks it queues drain in the same pass.
|
|
60
|
+
const root = kysely_transaction_context_js_1.kyselyTransactionScopes.createRoot(boundTrx);
|
|
61
|
+
const result = await kysely_transaction_context_js_1.kyselyTransactionScopes.run(narrowed, root, () => (0, kysely_transaction_context_js_1.runInKyselySavepoint)(narrowed, root, callback));
|
|
62
|
+
await kysely_transaction_context_js_1.kyselyTransactionScopes.run(narrowed, root, () => (0, core_1.runAfterCommitCallbacks)(root.callbacks));
|
|
63
|
+
return result;
|
|
64
|
+
},
|
|
65
|
+
afterCommit(callback) {
|
|
66
|
+
kysely_transaction_context_js_1.kyselyTransactionScopes.afterCommit(narrowed, callback);
|
|
52
67
|
},
|
|
53
68
|
};
|
|
54
69
|
const instanceStore = new kysely_instance_store_js_1.KyselyWorkflowInstanceStore(narrowed);
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import type { Kysely, Transaction } from "kysely";
|
|
2
|
+
import { ScopedTransactionContext, type TransactionScope } from "@duraflows/core";
|
|
2
3
|
import type { WorkflowDatabase } from "./kysely-database.js";
|
|
4
|
+
type WorkflowTransaction = Transaction<WorkflowDatabase>;
|
|
5
|
+
/** @internal Shared with the runners; not part of the public API. */
|
|
6
|
+
export declare const kyselyTransactionScopes: ScopedTransactionContext<Kysely<WorkflowDatabase>, WorkflowTransaction>;
|
|
7
|
+
/**
|
|
8
|
+
* @internal Runs `callback` in a savepoint nested in `scope`, issuing the
|
|
9
|
+
* savepoint statements on that scope's own transaction. Every kysely entry
|
|
10
|
+
* point that nests (the runners and `transaction()`) goes through here.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runInKyselySavepoint<T>(owner: Kysely<WorkflowDatabase>, scope: TransactionScope<WorkflowTransaction>, callback: () => Promise<T>): Promise<T>;
|
|
13
|
+
/**
|
|
14
|
+
* @internal Runs `callback` as the root of a new kysely transaction on `db`
|
|
15
|
+
* (after `setup`, e.g. transaction-local timeouts), and only once kysely has
|
|
16
|
+
* committed runs the after-commit callbacks queued inside. On error kysely
|
|
17
|
+
* rolls back and the callbacks are dropped.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runOwnedKyselyTransaction<T>(db: Kysely<WorkflowDatabase>, setup: (trx: WorkflowTransaction) => Promise<void>, callback: (trx: WorkflowTransaction) => Promise<T>): Promise<T>;
|
|
3
20
|
export declare const KyselyTransactionContext: {
|
|
4
|
-
getTransaction(owner: Kysely<WorkflowDatabase>):
|
|
21
|
+
getTransaction(owner: Kysely<WorkflowDatabase>): WorkflowTransaction | undefined;
|
|
5
22
|
/**
|
|
6
|
-
* Executes callback with `trx` as the active transaction for `owner`.
|
|
23
|
+
* Executes callback with `trx` as the active transaction for `owner`. You
|
|
24
|
+
* own the transaction. Workflow calls inside run in savepoints, and their
|
|
25
|
+
* observers fire when `callback`'s promise resolves (before your COMMIT);
|
|
26
|
+
* nothing fires if it rejects. Use {@link KyselyTransactionContext.transaction}
|
|
27
|
+
* for delivery strictly after COMMIT.
|
|
7
28
|
*
|
|
8
29
|
* Generic so callers can pass `Transaction<MyDb & WorkflowDatabase>`
|
|
9
30
|
* (Kysely is invariant in DB, so a non-generic signature would reject
|
|
@@ -12,5 +33,20 @@ export declare const KyselyTransactionContext: {
|
|
|
12
33
|
* workflow tables.
|
|
13
34
|
*/
|
|
14
35
|
run<T, DB extends WorkflowDatabase = WorkflowDatabase>(owner: Kysely<WorkflowDatabase>, trx: Transaction<DB>, callback: () => T): T;
|
|
36
|
+
/**
|
|
37
|
+
* Runs `callback` in a new transaction on `db` (`db.transaction().execute`),
|
|
38
|
+
* with that transaction active for workflow calls. Observers of workflow
|
|
39
|
+
* calls made inside fire after the transaction commits, and never if it
|
|
40
|
+
* rolls back.
|
|
41
|
+
*
|
|
42
|
+
* Called while a transaction for `db` is already active (an outer
|
|
43
|
+
* `transaction()`, a workflow call, or a seeded `run()`), it joins that
|
|
44
|
+
* transaction instead: `callback` receives the existing transaction and runs
|
|
45
|
+
* in a savepoint, which is released on success (its observers then wait for
|
|
46
|
+
* the enclosing transaction) and rolled back on failure (its observers are
|
|
47
|
+
* dropped).
|
|
48
|
+
*/
|
|
49
|
+
transaction<T, DB extends WorkflowDatabase = WorkflowDatabase>(db: Kysely<DB>, callback: (trx: Transaction<DB>) => Promise<T>): Promise<T>;
|
|
15
50
|
};
|
|
51
|
+
export {};
|
|
16
52
|
//# sourceMappingURL=kysely-transaction-context.d.ts.map
|
|
@@ -1,24 +1,70 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.KyselyTransactionContext = void 0;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
exports.KyselyTransactionContext = exports.kyselyTransactionScopes = void 0;
|
|
4
|
+
exports.runInKyselySavepoint = runInKyselySavepoint;
|
|
5
|
+
exports.runOwnedKyselyTransaction = runOwnedKyselyTransaction;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
const core_1 = require("@duraflows/core");
|
|
8
|
+
// One context per Kysely instance, so a transaction on db A is invisible to
|
|
9
|
+
// stores and runners bound to db B.
|
|
10
|
+
/** @internal Shared with the runners; not part of the public API. */
|
|
11
|
+
exports.kyselyTransactionScopes = new core_1.ScopedTransactionContext();
|
|
12
|
+
/**
|
|
13
|
+
* Runs one fixed SQL statement (a savepoint command) on `trx`.
|
|
14
|
+
*
|
|
15
|
+
* `Transaction` has no savepoint API in kysely 0.29 (only `ControlledTransaction`
|
|
16
|
+
* does), and this package imports kysely for types only, since kysely is
|
|
17
|
+
* ESM-only and a runtime import would break the CommonJS build. So the
|
|
18
|
+
* statement is handed to `executeQuery` as a compiled query built by hand,
|
|
19
|
+
* shaped exactly like `CompiledQuery.raw(sql)`. `sql` is always an internally
|
|
20
|
+
* generated `SAVEPOINT` / `RELEASE` / `ROLLBACK TO` statement, never user input.
|
|
21
|
+
*/
|
|
22
|
+
function executeRawStatement(trx, sql) {
|
|
23
|
+
const query = {
|
|
24
|
+
sql,
|
|
25
|
+
parameters: [],
|
|
26
|
+
query: { kind: "RawNode", sqlFragments: [sql], parameters: [] },
|
|
27
|
+
queryId: { queryId: `duraflows_${(0, node_crypto_1.randomUUID)()}` },
|
|
28
|
+
};
|
|
29
|
+
return trx.executeQuery(query);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @internal Runs `callback` in a savepoint nested in `scope`, issuing the
|
|
33
|
+
* savepoint statements on that scope's own transaction. Every kysely entry
|
|
34
|
+
* point that nests (the runners and `transaction()`) goes through here.
|
|
35
|
+
*/
|
|
36
|
+
function runInKyselySavepoint(owner, scope, callback) {
|
|
37
|
+
return exports.kyselyTransactionScopes.runInSavepoint(owner, scope, callback, (sql) => executeRawStatement(scope.connection, sql));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* @internal Runs `callback` as the root of a new kysely transaction on `db`
|
|
41
|
+
* (after `setup`, e.g. transaction-local timeouts), and only once kysely has
|
|
42
|
+
* committed runs the after-commit callbacks queued inside. On error kysely
|
|
43
|
+
* rolls back and the callbacks are dropped.
|
|
44
|
+
*/
|
|
45
|
+
async function runOwnedKyselyTransaction(db, setup, callback) {
|
|
46
|
+
let root;
|
|
47
|
+
const result = await db.transaction().execute(async (trx) => {
|
|
48
|
+
await setup(trx);
|
|
49
|
+
const scope = exports.kyselyTransactionScopes.createRoot(trx);
|
|
50
|
+
root = scope;
|
|
51
|
+
return exports.kyselyTransactionScopes.run(db, scope, () => callback(trx));
|
|
52
|
+
});
|
|
53
|
+
if (root) {
|
|
54
|
+
await (0, core_1.runAfterCommitCallbacks)(root.callbacks);
|
|
13
55
|
}
|
|
14
|
-
return
|
|
56
|
+
return result;
|
|
15
57
|
}
|
|
16
58
|
exports.KyselyTransactionContext = {
|
|
17
59
|
getTransaction(owner) {
|
|
18
|
-
return
|
|
60
|
+
return exports.kyselyTransactionScopes.current(owner)?.connection;
|
|
19
61
|
},
|
|
20
62
|
/**
|
|
21
|
-
* Executes callback with `trx` as the active transaction for `owner`.
|
|
63
|
+
* Executes callback with `trx` as the active transaction for `owner`. You
|
|
64
|
+
* own the transaction. Workflow calls inside run in savepoints, and their
|
|
65
|
+
* observers fire when `callback`'s promise resolves (before your COMMIT);
|
|
66
|
+
* nothing fires if it rejects. Use {@link KyselyTransactionContext.transaction}
|
|
67
|
+
* for delivery strictly after COMMIT.
|
|
22
68
|
*
|
|
23
69
|
* Generic so callers can pass `Transaction<MyDb & WorkflowDatabase>`
|
|
24
70
|
* (Kysely is invariant in DB, so a non-generic signature would reject
|
|
@@ -27,7 +73,28 @@ exports.KyselyTransactionContext = {
|
|
|
27
73
|
* workflow tables.
|
|
28
74
|
*/
|
|
29
75
|
run(owner, trx, callback) {
|
|
30
|
-
return
|
|
76
|
+
return exports.kyselyTransactionScopes.runSeeded(owner, trx, callback);
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* Runs `callback` in a new transaction on `db` (`db.transaction().execute`),
|
|
80
|
+
* with that transaction active for workflow calls. Observers of workflow
|
|
81
|
+
* calls made inside fire after the transaction commits, and never if it
|
|
82
|
+
* rolls back.
|
|
83
|
+
*
|
|
84
|
+
* Called while a transaction for `db` is already active (an outer
|
|
85
|
+
* `transaction()`, a workflow call, or a seeded `run()`), it joins that
|
|
86
|
+
* transaction instead: `callback` receives the existing transaction and runs
|
|
87
|
+
* in a savepoint, which is released on success (its observers then wait for
|
|
88
|
+
* the enclosing transaction) and rolled back on failure (its observers are
|
|
89
|
+
* dropped).
|
|
90
|
+
*/
|
|
91
|
+
transaction(db, callback) {
|
|
92
|
+
const owner = db;
|
|
93
|
+
const scope = exports.kyselyTransactionScopes.current(owner);
|
|
94
|
+
if (scope) {
|
|
95
|
+
return runInKyselySavepoint(owner, scope, () => callback(scope.connection));
|
|
96
|
+
}
|
|
97
|
+
return runOwnedKyselyTransaction(owner, async () => { }, (trx) => callback(trx));
|
|
31
98
|
},
|
|
32
99
|
};
|
|
33
100
|
//# sourceMappingURL=kysely-transaction-context.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Kysely } from "kysely";
|
|
2
|
-
import type { WorkflowTransactionRunner } from "@duraflows/core";
|
|
2
|
+
import type { AfterCommitCallback, WorkflowTransactionRunner } from "@duraflows/core";
|
|
3
3
|
import type { WorkflowDatabase } from "./kysely-database.js";
|
|
4
4
|
/**
|
|
5
5
|
* Transaction-scoped PostgreSQL timeouts.
|
|
@@ -31,6 +31,7 @@ export declare class KyselyTransactionRunner implements WorkflowTransactionRunne
|
|
|
31
31
|
private readonly timeoutSettings;
|
|
32
32
|
constructor(db: Kysely<WorkflowDatabase>, options?: KyselyTransactionRunnerOptions);
|
|
33
33
|
runInTransaction<T>(callback: () => Promise<T>): Promise<T>;
|
|
34
|
+
afterCommit(callback: AfterCommitCallback): void;
|
|
34
35
|
/**
|
|
35
36
|
* Applies the configured timeouts to `trx` as transaction-local settings, so
|
|
36
37
|
* they are reverted on COMMIT/ROLLBACK and never leak to other users of the
|
|
@@ -36,14 +36,16 @@ class KyselyTransactionRunner {
|
|
|
36
36
|
this.timeoutSettings = buildTimeoutSettings(options);
|
|
37
37
|
}
|
|
38
38
|
async runInTransaction(callback) {
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
|
|
39
|
+
const scope = kysely_transaction_context_js_1.kyselyTransactionScopes.current(this.db);
|
|
40
|
+
if (scope) {
|
|
41
|
+
// Nested: a savepoint on the outer transaction, so a failure rolls back
|
|
42
|
+
// only this call. The outer transaction's timeouts stay in force.
|
|
43
|
+
return (0, kysely_transaction_context_js_1.runInKyselySavepoint)(this.db, scope, callback);
|
|
42
44
|
}
|
|
43
|
-
return this.db
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
return (0, kysely_transaction_context_js_1.runOwnedKyselyTransaction)(this.db, (trx) => this.applyTimeouts(trx), () => callback());
|
|
46
|
+
}
|
|
47
|
+
afterCommit(callback) {
|
|
48
|
+
kysely_transaction_context_js_1.kyselyTransactionScopes.afterCommit(this.db, callback);
|
|
47
49
|
}
|
|
48
50
|
/**
|
|
49
51
|
* Applies the configured timeouts to `trx` as transaction-local settings, so
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { runAfterCommitCallbacks } from "@duraflows/core";
|
|
1
2
|
import { KyselyWorkflowInstanceStore } from "./kysely-instance-store.js";
|
|
2
3
|
import { KyselyWorkflowHistoryStore } from "./kysely-history-store.js";
|
|
3
4
|
import { KyselyWorkflowDefinitionStore } from "./kysely-definition-store.js";
|
|
4
5
|
import { KyselyTransactionRunner } from "./kysely-transaction-runner.js";
|
|
5
|
-
import {
|
|
6
|
+
import { kyselyTransactionScopes, runInKyselySavepoint } from "./kysely-transaction-context.js";
|
|
6
7
|
export { KyselyTransactionContext } from "./kysely-transaction-context.js";
|
|
7
8
|
export { KyselyTransactionRunner } from "./kysely-transaction-runner.js";
|
|
8
9
|
export { KyselyWorkflowInstanceStore } from "./kysely-instance-store.js";
|
|
@@ -31,14 +32,28 @@ export function kyselyWorkflowProviders(db, options = {}) {
|
|
|
31
32
|
*/
|
|
32
33
|
export function kyselyWorkflowProvidersFromTransaction(trx) {
|
|
33
34
|
const narrowed = trx;
|
|
35
|
+
const boundTrx = trx;
|
|
34
36
|
const transactionRunner = {
|
|
35
37
|
async runInTransaction(callback) {
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// Scoped to the bound trx; an unrelated ambient transaction from another
|
|
39
|
+
// provider must never supersede it.
|
|
40
|
+
const scope = kyselyTransactionScopes.current(narrowed);
|
|
41
|
+
if (scope) {
|
|
42
|
+
return runInKyselySavepoint(narrowed, scope, callback);
|
|
40
43
|
}
|
|
41
|
-
|
|
44
|
+
// The caller owns `trx`, so even this outermost call is nested in their
|
|
45
|
+
// transaction: run it in a savepoint so a failure leaves no partial
|
|
46
|
+
// writes behind, and deliver its observers once it resolves. `trx` is
|
|
47
|
+
// still open then, so the observers run inside the root scope: a
|
|
48
|
+
// duraflows call they make joins `trx` as a savepoint, and the
|
|
49
|
+
// callbacks it queues drain in the same pass.
|
|
50
|
+
const root = kyselyTransactionScopes.createRoot(boundTrx);
|
|
51
|
+
const result = await kyselyTransactionScopes.run(narrowed, root, () => runInKyselySavepoint(narrowed, root, callback));
|
|
52
|
+
await kyselyTransactionScopes.run(narrowed, root, () => runAfterCommitCallbacks(root.callbacks));
|
|
53
|
+
return result;
|
|
54
|
+
},
|
|
55
|
+
afterCommit(callback) {
|
|
56
|
+
kyselyTransactionScopes.afterCommit(narrowed, callback);
|
|
42
57
|
},
|
|
43
58
|
};
|
|
44
59
|
const instanceStore = new KyselyWorkflowInstanceStore(narrowed);
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import type { Kysely, Transaction } from "kysely";
|
|
2
|
+
import { ScopedTransactionContext, type TransactionScope } from "@duraflows/core";
|
|
2
3
|
import type { WorkflowDatabase } from "./kysely-database.js";
|
|
4
|
+
type WorkflowTransaction = Transaction<WorkflowDatabase>;
|
|
5
|
+
/** @internal Shared with the runners; not part of the public API. */
|
|
6
|
+
export declare const kyselyTransactionScopes: ScopedTransactionContext<Kysely<WorkflowDatabase>, WorkflowTransaction>;
|
|
7
|
+
/**
|
|
8
|
+
* @internal Runs `callback` in a savepoint nested in `scope`, issuing the
|
|
9
|
+
* savepoint statements on that scope's own transaction. Every kysely entry
|
|
10
|
+
* point that nests (the runners and `transaction()`) goes through here.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runInKyselySavepoint<T>(owner: Kysely<WorkflowDatabase>, scope: TransactionScope<WorkflowTransaction>, callback: () => Promise<T>): Promise<T>;
|
|
13
|
+
/**
|
|
14
|
+
* @internal Runs `callback` as the root of a new kysely transaction on `db`
|
|
15
|
+
* (after `setup`, e.g. transaction-local timeouts), and only once kysely has
|
|
16
|
+
* committed runs the after-commit callbacks queued inside. On error kysely
|
|
17
|
+
* rolls back and the callbacks are dropped.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runOwnedKyselyTransaction<T>(db: Kysely<WorkflowDatabase>, setup: (trx: WorkflowTransaction) => Promise<void>, callback: (trx: WorkflowTransaction) => Promise<T>): Promise<T>;
|
|
3
20
|
export declare const KyselyTransactionContext: {
|
|
4
|
-
getTransaction(owner: Kysely<WorkflowDatabase>):
|
|
21
|
+
getTransaction(owner: Kysely<WorkflowDatabase>): WorkflowTransaction | undefined;
|
|
5
22
|
/**
|
|
6
|
-
* Executes callback with `trx` as the active transaction for `owner`.
|
|
23
|
+
* Executes callback with `trx` as the active transaction for `owner`. You
|
|
24
|
+
* own the transaction. Workflow calls inside run in savepoints, and their
|
|
25
|
+
* observers fire when `callback`'s promise resolves (before your COMMIT);
|
|
26
|
+
* nothing fires if it rejects. Use {@link KyselyTransactionContext.transaction}
|
|
27
|
+
* for delivery strictly after COMMIT.
|
|
7
28
|
*
|
|
8
29
|
* Generic so callers can pass `Transaction<MyDb & WorkflowDatabase>`
|
|
9
30
|
* (Kysely is invariant in DB, so a non-generic signature would reject
|
|
@@ -12,5 +33,20 @@ export declare const KyselyTransactionContext: {
|
|
|
12
33
|
* workflow tables.
|
|
13
34
|
*/
|
|
14
35
|
run<T, DB extends WorkflowDatabase = WorkflowDatabase>(owner: Kysely<WorkflowDatabase>, trx: Transaction<DB>, callback: () => T): T;
|
|
36
|
+
/**
|
|
37
|
+
* Runs `callback` in a new transaction on `db` (`db.transaction().execute`),
|
|
38
|
+
* with that transaction active for workflow calls. Observers of workflow
|
|
39
|
+
* calls made inside fire after the transaction commits, and never if it
|
|
40
|
+
* rolls back.
|
|
41
|
+
*
|
|
42
|
+
* Called while a transaction for `db` is already active (an outer
|
|
43
|
+
* `transaction()`, a workflow call, or a seeded `run()`), it joins that
|
|
44
|
+
* transaction instead: `callback` receives the existing transaction and runs
|
|
45
|
+
* in a savepoint, which is released on success (its observers then wait for
|
|
46
|
+
* the enclosing transaction) and rolled back on failure (its observers are
|
|
47
|
+
* dropped).
|
|
48
|
+
*/
|
|
49
|
+
transaction<T, DB extends WorkflowDatabase = WorkflowDatabase>(db: Kysely<DB>, callback: (trx: Transaction<DB>) => Promise<T>): Promise<T>;
|
|
15
50
|
};
|
|
51
|
+
export {};
|
|
16
52
|
//# sourceMappingURL=kysely-transaction-context.d.ts.map
|
|
@@ -1,21 +1,65 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
// transaction on db A is invisible to
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ScopedTransactionContext, runAfterCommitCallbacks } from "@duraflows/core";
|
|
3
|
+
// One context per Kysely instance, so a transaction on db A is invisible to
|
|
4
|
+
// stores and runners bound to db B.
|
|
5
|
+
/** @internal Shared with the runners; not part of the public API. */
|
|
6
|
+
export const kyselyTransactionScopes = new ScopedTransactionContext();
|
|
7
|
+
/**
|
|
8
|
+
* Runs one fixed SQL statement (a savepoint command) on `trx`.
|
|
9
|
+
*
|
|
10
|
+
* `Transaction` has no savepoint API in kysely 0.29 (only `ControlledTransaction`
|
|
11
|
+
* does), and this package imports kysely for types only, since kysely is
|
|
12
|
+
* ESM-only and a runtime import would break the CommonJS build. So the
|
|
13
|
+
* statement is handed to `executeQuery` as a compiled query built by hand,
|
|
14
|
+
* shaped exactly like `CompiledQuery.raw(sql)`. `sql` is always an internally
|
|
15
|
+
* generated `SAVEPOINT` / `RELEASE` / `ROLLBACK TO` statement, never user input.
|
|
16
|
+
*/
|
|
17
|
+
function executeRawStatement(trx, sql) {
|
|
18
|
+
const query = {
|
|
19
|
+
sql,
|
|
20
|
+
parameters: [],
|
|
21
|
+
query: { kind: "RawNode", sqlFragments: [sql], parameters: [] },
|
|
22
|
+
queryId: { queryId: `duraflows_${randomUUID()}` },
|
|
23
|
+
};
|
|
24
|
+
return trx.executeQuery(query);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* @internal Runs `callback` in a savepoint nested in `scope`, issuing the
|
|
28
|
+
* savepoint statements on that scope's own transaction. Every kysely entry
|
|
29
|
+
* point that nests (the runners and `transaction()`) goes through here.
|
|
30
|
+
*/
|
|
31
|
+
export function runInKyselySavepoint(owner, scope, callback) {
|
|
32
|
+
return kyselyTransactionScopes.runInSavepoint(owner, scope, callback, (sql) => executeRawStatement(scope.connection, sql));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* @internal Runs `callback` as the root of a new kysely transaction on `db`
|
|
36
|
+
* (after `setup`, e.g. transaction-local timeouts), and only once kysely has
|
|
37
|
+
* committed runs the after-commit callbacks queued inside. On error kysely
|
|
38
|
+
* rolls back and the callbacks are dropped.
|
|
39
|
+
*/
|
|
40
|
+
export async function runOwnedKyselyTransaction(db, setup, callback) {
|
|
41
|
+
let root;
|
|
42
|
+
const result = await db.transaction().execute(async (trx) => {
|
|
43
|
+
await setup(trx);
|
|
44
|
+
const scope = kyselyTransactionScopes.createRoot(trx);
|
|
45
|
+
root = scope;
|
|
46
|
+
return kyselyTransactionScopes.run(db, scope, () => callback(trx));
|
|
47
|
+
});
|
|
48
|
+
if (root) {
|
|
49
|
+
await runAfterCommitCallbacks(root.callbacks);
|
|
10
50
|
}
|
|
11
|
-
return
|
|
51
|
+
return result;
|
|
12
52
|
}
|
|
13
53
|
export const KyselyTransactionContext = {
|
|
14
54
|
getTransaction(owner) {
|
|
15
|
-
return
|
|
55
|
+
return kyselyTransactionScopes.current(owner)?.connection;
|
|
16
56
|
},
|
|
17
57
|
/**
|
|
18
|
-
* Executes callback with `trx` as the active transaction for `owner`.
|
|
58
|
+
* Executes callback with `trx` as the active transaction for `owner`. You
|
|
59
|
+
* own the transaction. Workflow calls inside run in savepoints, and their
|
|
60
|
+
* observers fire when `callback`'s promise resolves (before your COMMIT);
|
|
61
|
+
* nothing fires if it rejects. Use {@link KyselyTransactionContext.transaction}
|
|
62
|
+
* for delivery strictly after COMMIT.
|
|
19
63
|
*
|
|
20
64
|
* Generic so callers can pass `Transaction<MyDb & WorkflowDatabase>`
|
|
21
65
|
* (Kysely is invariant in DB, so a non-generic signature would reject
|
|
@@ -24,7 +68,28 @@ export const KyselyTransactionContext = {
|
|
|
24
68
|
* workflow tables.
|
|
25
69
|
*/
|
|
26
70
|
run(owner, trx, callback) {
|
|
27
|
-
return
|
|
71
|
+
return kyselyTransactionScopes.runSeeded(owner, trx, callback);
|
|
72
|
+
},
|
|
73
|
+
/**
|
|
74
|
+
* Runs `callback` in a new transaction on `db` (`db.transaction().execute`),
|
|
75
|
+
* with that transaction active for workflow calls. Observers of workflow
|
|
76
|
+
* calls made inside fire after the transaction commits, and never if it
|
|
77
|
+
* rolls back.
|
|
78
|
+
*
|
|
79
|
+
* Called while a transaction for `db` is already active (an outer
|
|
80
|
+
* `transaction()`, a workflow call, or a seeded `run()`), it joins that
|
|
81
|
+
* transaction instead: `callback` receives the existing transaction and runs
|
|
82
|
+
* in a savepoint, which is released on success (its observers then wait for
|
|
83
|
+
* the enclosing transaction) and rolled back on failure (its observers are
|
|
84
|
+
* dropped).
|
|
85
|
+
*/
|
|
86
|
+
transaction(db, callback) {
|
|
87
|
+
const owner = db;
|
|
88
|
+
const scope = kyselyTransactionScopes.current(owner);
|
|
89
|
+
if (scope) {
|
|
90
|
+
return runInKyselySavepoint(owner, scope, () => callback(scope.connection));
|
|
91
|
+
}
|
|
92
|
+
return runOwnedKyselyTransaction(owner, async () => { }, (trx) => callback(trx));
|
|
28
93
|
},
|
|
29
94
|
};
|
|
30
95
|
//# sourceMappingURL=kysely-transaction-context.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Kysely } from "kysely";
|
|
2
|
-
import type { WorkflowTransactionRunner } from "@duraflows/core";
|
|
2
|
+
import type { AfterCommitCallback, WorkflowTransactionRunner } from "@duraflows/core";
|
|
3
3
|
import type { WorkflowDatabase } from "./kysely-database.js";
|
|
4
4
|
/**
|
|
5
5
|
* Transaction-scoped PostgreSQL timeouts.
|
|
@@ -31,6 +31,7 @@ export declare class KyselyTransactionRunner implements WorkflowTransactionRunne
|
|
|
31
31
|
private readonly timeoutSettings;
|
|
32
32
|
constructor(db: Kysely<WorkflowDatabase>, options?: KyselyTransactionRunnerOptions);
|
|
33
33
|
runInTransaction<T>(callback: () => Promise<T>): Promise<T>;
|
|
34
|
+
afterCommit(callback: AfterCommitCallback): void;
|
|
34
35
|
/**
|
|
35
36
|
* Applies the configured timeouts to `trx` as transaction-local settings, so
|
|
36
37
|
* they are reverted on COMMIT/ROLLBACK and never leak to other users of the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WorkflowError } from "@duraflows/core";
|
|
2
|
-
import {
|
|
2
|
+
import { kyselyTransactionScopes, runInKyselySavepoint, runOwnedKyselyTransaction, } from "./kysely-transaction-context.js";
|
|
3
3
|
/**
|
|
4
4
|
* Rejects anything that is not a finite, non-negative integer. Called at
|
|
5
5
|
* construction time so an invalid value can never reach the database.
|
|
@@ -33,14 +33,16 @@ export class KyselyTransactionRunner {
|
|
|
33
33
|
this.timeoutSettings = buildTimeoutSettings(options);
|
|
34
34
|
}
|
|
35
35
|
async runInTransaction(callback) {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
|
|
36
|
+
const scope = kyselyTransactionScopes.current(this.db);
|
|
37
|
+
if (scope) {
|
|
38
|
+
// Nested: a savepoint on the outer transaction, so a failure rolls back
|
|
39
|
+
// only this call. The outer transaction's timeouts stay in force.
|
|
40
|
+
return runInKyselySavepoint(this.db, scope, callback);
|
|
39
41
|
}
|
|
40
|
-
return this.db
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
return runOwnedKyselyTransaction(this.db, (trx) => this.applyTimeouts(trx), () => callback());
|
|
43
|
+
}
|
|
44
|
+
afterCommit(callback) {
|
|
45
|
+
kyselyTransactionScopes.afterCommit(this.db, callback);
|
|
44
46
|
}
|
|
45
47
|
/**
|
|
46
48
|
* Applies the configured timeouts to `trx` as transaction-local settings, so
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duraflows/kysely",
|
|
3
|
-
"version": "5.1
|
|
3
|
+
"version": "5.2.1",
|
|
4
4
|
"description": "PostgreSQL persistence adapter for duraflows using `kysely`. Integrates with kysely's type-safe query builder and AsyncLocalStorage-based transaction context.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"duraflows",
|
|
@@ -51,15 +51,15 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@duraflows/core": "^5.1
|
|
55
|
-
"kysely": "^0.29.
|
|
54
|
+
"@duraflows/core": "^5.2.1",
|
|
55
|
+
"kysely": "^0.29.6"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@types/pg": "^8.
|
|
59
|
-
"kysely": "^0.29.
|
|
58
|
+
"@types/pg": "^8.23.1",
|
|
59
|
+
"kysely": "^0.29.6",
|
|
60
60
|
"pg": "^8.23.0",
|
|
61
|
-
"@duraflows/core": "5.1
|
|
62
|
-
"@duraflows/pg": "5.1
|
|
61
|
+
"@duraflows/core": "5.2.1",
|
|
62
|
+
"@duraflows/pg": "5.2.1"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"build": "tsc --build"
|