@duraflows/kysely 5.0.0 → 5.2.0

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 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
- // Seed the transaction context so Duraflows participates in your transaction
53
- await db.transaction().execute(async (trx) => {
54
- await KyselyTransactionContext.run(db, trx, async () => {
55
- // Your own writes -- uses trx
56
- await trx.insertInto("orders").values({ id: "ORD-1", status: "paid" }).execute();
57
-
58
- // Duraflows writes -- also uses trx (same transaction)
59
- await runtime.triggerEvent({
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 rollback -- no split-brain
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:** Reuses it (no nested transaction)
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,7 @@ 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. The returned transaction runner seeds the context with the bound `trx` if no context already exists.
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.
131
146
 
132
147
  ### `KyselyTransactionContext`
133
148
 
@@ -135,6 +150,7 @@ The context is scoped per Kysely instance:
135
150
 
136
151
  - `getTransaction(db)` -- Returns the active `Transaction<WorkflowDatabase>` for the given `db` instance, or `undefined`
137
152
  - `run(db, trx, callback)` -- Executes `callback` with `trx` as the active transaction context for the given `db` instance
153
+ - `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
154
 
139
155
  ## Documentation
140
156
 
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
- // Nested calls reuse the bound transaction; an unrelated ambient
47
- // transaction from another provider must never supersede it.
48
- if (kysely_transaction_context_js_1.KyselyTransactionContext.getTransaction(narrowed)) {
49
- return callback();
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 kysely_transaction_context_js_1.kyselyTransactionScopes.runInSavepoint(narrowed, scope, callback, (sql) => (0, kysely_transaction_context_js_1.executeRawStatement)(scope.connection, sql));
50
53
  }
51
- return kysely_transaction_context_js_1.KyselyTransactionContext.run(narrowed, trx, callback);
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, () => kysely_transaction_context_js_1.kyselyTransactionScopes.runInSavepoint(narrowed, root, callback, (sql) => (0, kysely_transaction_context_js_1.executeRawStatement)(boundTrx, sql)));
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,35 @@
1
1
  import type { Kysely, Transaction } from "kysely";
2
+ import { ScopedTransactionContext } 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 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
+ export declare function executeRawStatement(trx: WorkflowTransaction, sql: string): Promise<unknown>;
18
+ /**
19
+ * @internal Runs `callback` as the root of a new kysely transaction on `db`
20
+ * (after `setup`, e.g. transaction-local timeouts), and only once kysely has
21
+ * committed runs the after-commit callbacks queued inside. On error kysely
22
+ * rolls back and the callbacks are dropped.
23
+ */
24
+ export declare function runOwnedKyselyTransaction<T>(db: Kysely<WorkflowDatabase>, setup: (trx: WorkflowTransaction) => Promise<void>, callback: (trx: WorkflowTransaction) => Promise<T>): Promise<T>;
3
25
  export declare const KyselyTransactionContext: {
4
- getTransaction(owner: Kysely<WorkflowDatabase>): Transaction<WorkflowDatabase> | undefined;
26
+ getTransaction(owner: Kysely<WorkflowDatabase>): WorkflowTransaction | undefined;
5
27
  /**
6
- * Executes callback with `trx` as the active transaction for `owner`.
28
+ * Executes callback with `trx` as the active transaction for `owner`. You
29
+ * own the transaction. Workflow calls inside run in savepoints, and their
30
+ * observers fire when `callback`'s promise resolves (before your COMMIT);
31
+ * nothing fires if it rejects. Use {@link KyselyTransactionContext.transaction}
32
+ * for delivery strictly after COMMIT.
7
33
  *
8
34
  * Generic so callers can pass `Transaction<MyDb & WorkflowDatabase>`
9
35
  * (Kysely is invariant in DB, so a non-generic signature would reject
@@ -12,5 +38,20 @@ export declare const KyselyTransactionContext: {
12
38
  * workflow tables.
13
39
  */
14
40
  run<T, DB extends WorkflowDatabase = WorkflowDatabase>(owner: Kysely<WorkflowDatabase>, trx: Transaction<DB>, callback: () => T): T;
41
+ /**
42
+ * Runs `callback` in a new transaction on `db` (`db.transaction().execute`),
43
+ * with that transaction active for workflow calls. Observers of workflow
44
+ * calls made inside fire after the transaction commits, and never if it
45
+ * rolls back.
46
+ *
47
+ * Called while a transaction for `db` is already active (an outer
48
+ * `transaction()`, a workflow call, or a seeded `run()`), it joins that
49
+ * transaction instead: `callback` receives the existing transaction and runs
50
+ * in a savepoint, which is released on success (its observers then wait for
51
+ * the enclosing transaction) and rolled back on failure (its observers are
52
+ * dropped).
53
+ */
54
+ transaction<T, DB extends WorkflowDatabase = WorkflowDatabase>(db: Kysely<DB>, callback: (trx: Transaction<DB>) => Promise<T>): Promise<T>;
15
55
  };
56
+ export {};
16
57
  //# sourceMappingURL=kysely-transaction-context.d.ts.map
@@ -1,24 +1,62 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.KyselyTransactionContext = void 0;
4
- const node_async_hooks_1 = require("node:async_hooks");
5
- // One context per Kysely instance (or per pre-bound Transaction): a
6
- // transaction on db A is invisible to stores and runners bound to db B.
7
- const storages = new WeakMap();
8
- function storageFor(owner) {
9
- let storage = storages.get(owner);
10
- if (!storage) {
11
- storage = new node_async_hooks_1.AsyncLocalStorage();
12
- storages.set(owner, storage);
3
+ exports.KyselyTransactionContext = exports.kyselyTransactionScopes = void 0;
4
+ exports.executeRawStatement = executeRawStatement;
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
+ * @internal 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` as the root of a new kysely transaction on `db`
33
+ * (after `setup`, e.g. transaction-local timeouts), and only once kysely has
34
+ * committed runs the after-commit callbacks queued inside. On error kysely
35
+ * rolls back and the callbacks are dropped.
36
+ */
37
+ async function runOwnedKyselyTransaction(db, setup, callback) {
38
+ let root;
39
+ const result = await db.transaction().execute(async (trx) => {
40
+ await setup(trx);
41
+ const scope = exports.kyselyTransactionScopes.createRoot(trx);
42
+ root = scope;
43
+ return exports.kyselyTransactionScopes.run(db, scope, () => callback(trx));
44
+ });
45
+ if (root) {
46
+ await (0, core_1.runAfterCommitCallbacks)(root.callbacks);
13
47
  }
14
- return storage;
48
+ return result;
15
49
  }
16
50
  exports.KyselyTransactionContext = {
17
51
  getTransaction(owner) {
18
- return storages.get(owner)?.getStore();
52
+ return exports.kyselyTransactionScopes.current(owner)?.connection;
19
53
  },
20
54
  /**
21
- * Executes callback with `trx` as the active transaction for `owner`.
55
+ * Executes callback with `trx` as the active transaction for `owner`. You
56
+ * own the transaction. Workflow calls inside run in savepoints, and their
57
+ * observers fire when `callback`'s promise resolves (before your COMMIT);
58
+ * nothing fires if it rejects. Use {@link KyselyTransactionContext.transaction}
59
+ * for delivery strictly after COMMIT.
22
60
  *
23
61
  * Generic so callers can pass `Transaction<MyDb & WorkflowDatabase>`
24
62
  * (Kysely is invariant in DB, so a non-generic signature would reject
@@ -27,7 +65,28 @@ exports.KyselyTransactionContext = {
27
65
  * workflow tables.
28
66
  */
29
67
  run(owner, trx, callback) {
30
- return storageFor(owner).run(trx, callback);
68
+ return exports.kyselyTransactionScopes.runSeeded(owner, trx, callback);
69
+ },
70
+ /**
71
+ * Runs `callback` in a new transaction on `db` (`db.transaction().execute`),
72
+ * with that transaction active for workflow calls. Observers of workflow
73
+ * calls made inside fire after the transaction commits, and never if it
74
+ * rolls back.
75
+ *
76
+ * Called while a transaction for `db` is already active (an outer
77
+ * `transaction()`, a workflow call, or a seeded `run()`), it joins that
78
+ * transaction instead: `callback` receives the existing transaction and runs
79
+ * in a savepoint, which is released on success (its observers then wait for
80
+ * the enclosing transaction) and rolled back on failure (its observers are
81
+ * dropped).
82
+ */
83
+ transaction(db, callback) {
84
+ const owner = db;
85
+ const scope = exports.kyselyTransactionScopes.current(owner);
86
+ if (scope) {
87
+ return exports.kyselyTransactionScopes.runInSavepoint(owner, scope, () => callback(scope.connection), (sql) => executeRawStatement(scope.connection, sql));
88
+ }
89
+ return runOwnedKyselyTransaction(owner, async () => { }, (trx) => callback(trx));
31
90
  },
32
91
  };
33
92
  //# 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 existing = kysely_transaction_context_js_1.KyselyTransactionContext.getTransaction(this.db);
40
- if (existing) {
41
- return callback();
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 kysely_transaction_context_js_1.kyselyTransactionScopes.runInSavepoint(this.db, scope, callback, (sql) => (0, kysely_transaction_context_js_1.executeRawStatement)(scope.connection, sql));
42
44
  }
43
- return this.db.transaction().execute(async (trx) => {
44
- await this.applyTimeouts(trx);
45
- return kysely_transaction_context_js_1.KyselyTransactionContext.run(this.db, trx, callback);
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 { KyselyTransactionContext } from "./kysely-transaction-context.js";
6
+ import { executeRawStatement, kyselyTransactionScopes } 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
- // Nested calls reuse the bound transaction; an unrelated ambient
37
- // transaction from another provider must never supersede it.
38
- if (KyselyTransactionContext.getTransaction(narrowed)) {
39
- return callback();
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 kyselyTransactionScopes.runInSavepoint(narrowed, scope, callback, (sql) => executeRawStatement(scope.connection, sql));
40
43
  }
41
- return KyselyTransactionContext.run(narrowed, trx, callback);
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, () => kyselyTransactionScopes.runInSavepoint(narrowed, root, callback, (sql) => executeRawStatement(boundTrx, sql)));
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,35 @@
1
1
  import type { Kysely, Transaction } from "kysely";
2
+ import { ScopedTransactionContext } 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 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
+ export declare function executeRawStatement(trx: WorkflowTransaction, sql: string): Promise<unknown>;
18
+ /**
19
+ * @internal Runs `callback` as the root of a new kysely transaction on `db`
20
+ * (after `setup`, e.g. transaction-local timeouts), and only once kysely has
21
+ * committed runs the after-commit callbacks queued inside. On error kysely
22
+ * rolls back and the callbacks are dropped.
23
+ */
24
+ export declare function runOwnedKyselyTransaction<T>(db: Kysely<WorkflowDatabase>, setup: (trx: WorkflowTransaction) => Promise<void>, callback: (trx: WorkflowTransaction) => Promise<T>): Promise<T>;
3
25
  export declare const KyselyTransactionContext: {
4
- getTransaction(owner: Kysely<WorkflowDatabase>): Transaction<WorkflowDatabase> | undefined;
26
+ getTransaction(owner: Kysely<WorkflowDatabase>): WorkflowTransaction | undefined;
5
27
  /**
6
- * Executes callback with `trx` as the active transaction for `owner`.
28
+ * Executes callback with `trx` as the active transaction for `owner`. You
29
+ * own the transaction. Workflow calls inside run in savepoints, and their
30
+ * observers fire when `callback`'s promise resolves (before your COMMIT);
31
+ * nothing fires if it rejects. Use {@link KyselyTransactionContext.transaction}
32
+ * for delivery strictly after COMMIT.
7
33
  *
8
34
  * Generic so callers can pass `Transaction<MyDb & WorkflowDatabase>`
9
35
  * (Kysely is invariant in DB, so a non-generic signature would reject
@@ -12,5 +38,20 @@ export declare const KyselyTransactionContext: {
12
38
  * workflow tables.
13
39
  */
14
40
  run<T, DB extends WorkflowDatabase = WorkflowDatabase>(owner: Kysely<WorkflowDatabase>, trx: Transaction<DB>, callback: () => T): T;
41
+ /**
42
+ * Runs `callback` in a new transaction on `db` (`db.transaction().execute`),
43
+ * with that transaction active for workflow calls. Observers of workflow
44
+ * calls made inside fire after the transaction commits, and never if it
45
+ * rolls back.
46
+ *
47
+ * Called while a transaction for `db` is already active (an outer
48
+ * `transaction()`, a workflow call, or a seeded `run()`), it joins that
49
+ * transaction instead: `callback` receives the existing transaction and runs
50
+ * in a savepoint, which is released on success (its observers then wait for
51
+ * the enclosing transaction) and rolled back on failure (its observers are
52
+ * dropped).
53
+ */
54
+ transaction<T, DB extends WorkflowDatabase = WorkflowDatabase>(db: Kysely<DB>, callback: (trx: Transaction<DB>) => Promise<T>): Promise<T>;
15
55
  };
56
+ export {};
16
57
  //# sourceMappingURL=kysely-transaction-context.d.ts.map
@@ -1,21 +1,57 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
- // One context per Kysely instance (or per pre-bound Transaction): a
3
- // transaction on db A is invisible to stores and runners bound to db B.
4
- const storages = new WeakMap();
5
- function storageFor(owner) {
6
- let storage = storages.get(owner);
7
- if (!storage) {
8
- storage = new AsyncLocalStorage();
9
- storages.set(owner, storage);
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
+ * @internal 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
+ export 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` as the root of a new kysely transaction on `db`
28
+ * (after `setup`, e.g. transaction-local timeouts), and only once kysely has
29
+ * committed runs the after-commit callbacks queued inside. On error kysely
30
+ * rolls back and the callbacks are dropped.
31
+ */
32
+ export async function runOwnedKyselyTransaction(db, setup, callback) {
33
+ let root;
34
+ const result = await db.transaction().execute(async (trx) => {
35
+ await setup(trx);
36
+ const scope = kyselyTransactionScopes.createRoot(trx);
37
+ root = scope;
38
+ return kyselyTransactionScopes.run(db, scope, () => callback(trx));
39
+ });
40
+ if (root) {
41
+ await runAfterCommitCallbacks(root.callbacks);
10
42
  }
11
- return storage;
43
+ return result;
12
44
  }
13
45
  export const KyselyTransactionContext = {
14
46
  getTransaction(owner) {
15
- return storages.get(owner)?.getStore();
47
+ return kyselyTransactionScopes.current(owner)?.connection;
16
48
  },
17
49
  /**
18
- * Executes callback with `trx` as the active transaction for `owner`.
50
+ * Executes callback with `trx` as the active transaction for `owner`. You
51
+ * own the transaction. Workflow calls inside run in savepoints, and their
52
+ * observers fire when `callback`'s promise resolves (before your COMMIT);
53
+ * nothing fires if it rejects. Use {@link KyselyTransactionContext.transaction}
54
+ * for delivery strictly after COMMIT.
19
55
  *
20
56
  * Generic so callers can pass `Transaction<MyDb & WorkflowDatabase>`
21
57
  * (Kysely is invariant in DB, so a non-generic signature would reject
@@ -24,7 +60,28 @@ export const KyselyTransactionContext = {
24
60
  * workflow tables.
25
61
  */
26
62
  run(owner, trx, callback) {
27
- return storageFor(owner).run(trx, callback);
63
+ return kyselyTransactionScopes.runSeeded(owner, trx, callback);
64
+ },
65
+ /**
66
+ * Runs `callback` in a new transaction on `db` (`db.transaction().execute`),
67
+ * with that transaction active for workflow calls. Observers of workflow
68
+ * calls made inside fire after the transaction commits, and never if it
69
+ * rolls back.
70
+ *
71
+ * Called while a transaction for `db` is already active (an outer
72
+ * `transaction()`, a workflow call, or a seeded `run()`), it joins that
73
+ * transaction instead: `callback` receives the existing transaction and runs
74
+ * in a savepoint, which is released on success (its observers then wait for
75
+ * the enclosing transaction) and rolled back on failure (its observers are
76
+ * dropped).
77
+ */
78
+ transaction(db, callback) {
79
+ const owner = db;
80
+ const scope = kyselyTransactionScopes.current(owner);
81
+ if (scope) {
82
+ return kyselyTransactionScopes.runInSavepoint(owner, scope, () => callback(scope.connection), (sql) => executeRawStatement(scope.connection, sql));
83
+ }
84
+ return runOwnedKyselyTransaction(owner, async () => { }, (trx) => callback(trx));
28
85
  },
29
86
  };
30
87
  //# 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 { KyselyTransactionContext } from "./kysely-transaction-context.js";
2
+ import { executeRawStatement, kyselyTransactionScopes, 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 existing = KyselyTransactionContext.getTransaction(this.db);
37
- if (existing) {
38
- return callback();
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 kyselyTransactionScopes.runInSavepoint(this.db, scope, callback, (sql) => executeRawStatement(scope.connection, sql));
39
41
  }
40
- return this.db.transaction().execute(async (trx) => {
41
- await this.applyTimeouts(trx);
42
- return KyselyTransactionContext.run(this.db, trx, callback);
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.0.0",
3
+ "version": "5.2.0",
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.0.0",
55
- "kysely": "^0.29.5"
54
+ "@duraflows/core": "^5.2.0",
55
+ "kysely": "^0.29.6"
56
56
  },
57
57
  "devDependencies": {
58
- "@types/pg": "^8.21.0",
59
- "kysely": "^0.29.5",
58
+ "@types/pg": "^8.23.1",
59
+ "kysely": "^0.29.6",
60
60
  "pg": "^8.23.0",
61
- "@duraflows/core": "5.0.0",
62
- "@duraflows/pg": "5.0.0"
61
+ "@duraflows/core": "5.2.0",
62
+ "@duraflows/pg": "5.2.0"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "tsc --build"