@zudojs/transactions 0.1.0 → 1.1.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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -11
  3. package/dist/adapter/adapter.core.d.ts +4 -0
  4. package/dist/adapter/adapter.core.js +45 -9
  5. package/dist/context/context.core.js +3 -0
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/manager/index.d.ts +7 -0
  9. package/dist/manager/index.js +5 -0
  10. package/dist/manager/manager.capabilities.d.ts +26 -0
  11. package/dist/manager/manager.capabilities.js +41 -0
  12. package/dist/manager/manager.commit.d.ts +19 -2
  13. package/dist/manager/manager.commit.js +121 -24
  14. package/dist/manager/manager.core.d.ts +34 -3
  15. package/dist/manager/manager.core.js +132 -53
  16. package/dist/manager/manager.events.d.ts +31 -0
  17. package/dist/manager/manager.events.js +45 -0
  18. package/dist/manager/manager.propagation.d.ts +33 -5
  19. package/dist/manager/manager.propagation.js +102 -36
  20. package/dist/manager/manager.retry.d.ts +20 -0
  21. package/dist/manager/manager.retry.js +46 -0
  22. package/dist/transaction/index.d.ts +5 -3
  23. package/dist/transaction/index.js +4 -3
  24. package/dist/transaction/transaction.core.d.ts +6 -1
  25. package/dist/transaction/transaction.core.js +64 -57
  26. package/dist/transaction/transaction.internal.d.ts +85 -0
  27. package/dist/transaction/transaction.internal.js +82 -0
  28. package/dist/transaction/transaction.participant.d.ts +31 -0
  29. package/dist/transaction/transaction.participant.js +99 -0
  30. package/dist/transaction/transactionStateMachine.d.ts +10 -0
  31. package/dist/transaction/transactionStateMachine.js +16 -2
  32. package/dist/transactionTypes/index.d.ts +4 -4
  33. package/dist/transactionTypes/transaction.interface.d.ts +19 -1
  34. package/dist/transactionTypes/transactionAdapter.d.ts +7 -9
  35. package/dist/transactionTypes/transactionHooks.d.ts +0 -7
  36. package/dist/transactionTypes/transactionState.d.ts +9 -0
  37. package/dist/utils/utils.helper.d.ts +6 -1
  38. package/dist/utils/utils.helper.js +8 -2
  39. package/package.json +24 -13
  40. package/dist/.tsbuildinfo +0 -1
  41. package/dist/adapter/adapter.core.d.ts.map +0 -1
  42. package/dist/adapter/adapter.core.js.map +0 -1
  43. package/dist/adapter/index.d.ts.map +0 -1
  44. package/dist/adapter/index.js.map +0 -1
  45. package/dist/context/context.core.d.ts.map +0 -1
  46. package/dist/context/context.core.js.map +0 -1
  47. package/dist/context/index.d.ts.map +0 -1
  48. package/dist/context/index.js.map +0 -1
  49. package/dist/hooks/hooks.core.d.ts.map +0 -1
  50. package/dist/hooks/hooks.core.js.map +0 -1
  51. package/dist/hooks/index.d.ts.map +0 -1
  52. package/dist/hooks/index.js.map +0 -1
  53. package/dist/index.d.ts.map +0 -1
  54. package/dist/index.js.map +0 -1
  55. package/dist/manager/index.d.ts.map +0 -1
  56. package/dist/manager/index.js.map +0 -1
  57. package/dist/manager/manager.commit.d.ts.map +0 -1
  58. package/dist/manager/manager.commit.js.map +0 -1
  59. package/dist/manager/manager.core.d.ts.map +0 -1
  60. package/dist/manager/manager.core.js.map +0 -1
  61. package/dist/manager/manager.propagation.d.ts.map +0 -1
  62. package/dist/manager/manager.propagation.js.map +0 -1
  63. package/dist/registry/index.d.ts.map +0 -1
  64. package/dist/registry/index.js.map +0 -1
  65. package/dist/registry/registry.core.d.ts.map +0 -1
  66. package/dist/registry/registry.core.js.map +0 -1
  67. package/dist/transaction/index.d.ts.map +0 -1
  68. package/dist/transaction/index.js.map +0 -1
  69. package/dist/transaction/transaction.core.d.ts.map +0 -1
  70. package/dist/transaction/transaction.core.js.map +0 -1
  71. package/dist/transaction/transactionStateMachine.d.ts.map +0 -1
  72. package/dist/transaction/transactionStateMachine.js.map +0 -1
  73. package/dist/transactionErrors/index.d.ts.map +0 -1
  74. package/dist/transactionErrors/index.js.map +0 -1
  75. package/dist/transactionErrors/transactionError.base.d.ts.map +0 -1
  76. package/dist/transactionErrors/transactionError.base.js.map +0 -1
  77. package/dist/transactionErrors/transactionError.types.d.ts.map +0 -1
  78. package/dist/transactionErrors/transactionError.types.js.map +0 -1
  79. package/dist/transactionTypes/index.d.ts.map +0 -1
  80. package/dist/transactionTypes/index.js.map +0 -1
  81. package/dist/transactionTypes/transaction.interface.d.ts.map +0 -1
  82. package/dist/transactionTypes/transaction.interface.js.map +0 -1
  83. package/dist/transactionTypes/transactionAdapter.d.ts.map +0 -1
  84. package/dist/transactionTypes/transactionAdapter.js.map +0 -1
  85. package/dist/transactionTypes/transactionHooks.d.ts.map +0 -1
  86. package/dist/transactionTypes/transactionHooks.js.map +0 -1
  87. package/dist/transactionTypes/transactionState.d.ts.map +0 -1
  88. package/dist/transactionTypes/transactionState.js.map +0 -1
  89. package/dist/utils/index.d.ts.map +0 -1
  90. package/dist/utils/index.js.map +0 -1
  91. package/dist/utils/utils.helper.d.ts.map +0 -1
  92. package/dist/utils/utils.helper.js.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zudojs Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -13,28 +13,108 @@ npm install @zudojs/transactions
13
13
  ```typescript
14
14
  import { createTransactionManager } from "@zudojs/transactions";
15
15
 
16
- const manager = createTransactionManager({
17
- adapter: databaseAdapter,
16
+ const manager = createTransactionManager({ adapter: databaseAdapter });
17
+
18
+ // `run` opens a transaction, commits on success, rolls back on a throw.
19
+ const orderId = await manager.run(async (transaction) => {
20
+ await insertOrder(transaction);
21
+ await insertLineItems(transaction);
22
+ return "o_1";
18
23
  });
19
24
 
20
- await manager.run(async (tx) => {
21
- await tx.execute("INSERT INTO users ...");
22
- await tx.execute("INSERT INTO profiles ...");
25
+ // A nested `run` joins the enclosing transaction rather than completing it.
26
+ await manager.run(async () => {
27
+ await manager.run(async (participant) => {
28
+ // participant.kind === "participant"; committing it is a no-op, and a
29
+ // throw here marks the enclosing transaction rollback-only.
30
+ });
31
+ });
32
+
33
+ // Retries replay the whole unit of work.
34
+ await manager.run(handler, {
35
+ retry: {
36
+ attempts: 3,
37
+ backoff: "exponential",
38
+ delay: 50,
39
+ shouldRetry: (error) => isSerializationFailure(error),
40
+ },
41
+ });
42
+ ```
43
+
44
+ ## Propagation
45
+
46
+ | Mode | Transaction in progress | None in progress |
47
+ | --------------- | ---------------------------- | ------------------------ |
48
+ | `required` | joins it as a participant | opens a new one |
49
+ | `requires_new` | suspends it, opens a new one | opens a new one |
50
+ | `nested` | opens a savepoint on it | opens a new one |
51
+ | `supports` | joins it as a participant | runs non-transactionally |
52
+ | `not_supported` | suspends it | runs non-transactionally |
53
+ | `mandatory` | joins it as a participant | throws |
54
+ | `never` | throws | runs non-transactionally |
55
+
56
+ ## Lifecycle events
57
+
58
+ Pass `onEvent` to observe the lifecycle. Each event names a member of
59
+ `TRANSACTION_EVENTS` and carries the transaction id, a timestamp and the
60
+ elapsed duration:
61
+
62
+ ```typescript
63
+ const manager = createTransactionManager({
64
+ adapter,
65
+ onEvent: (event) => metrics.increment(event.type, { id: event.transactionId }),
23
66
  });
24
67
  ```
25
68
 
69
+ Emitted: `started`, `committing`, `committed`, `rolling_back`, `rolled_back`,
70
+ `failed` and `timed_out`. A throwing observer is ignored rather than failing
71
+ the transaction.
72
+
73
+ ## Errors
74
+
75
+ | Condition | Error |
76
+ | -------------------------------------- | ------------------------------ |
77
+ | Transaction outlived its `timeout` | `TransactionTimeoutError` |
78
+ | Marked rollback-only, then committed | `TransactionRollbackError` |
79
+ | Adapter lacks the requested isolation | `TransactionIsolationError` |
80
+ | Adapter lacks another requested feature | `TransactionCapabilityError` |
81
+ | Savepoint create/rollback/release fails | `SavepointError` |
82
+ | Propagation precondition violated | `TransactionPropagationError` |
83
+ | Operation invalid for the current state | `TransactionStateError` |
84
+ | Adapter refused the commit | `TransactionCommitError` |
85
+ | Adapter itself failed | `TransactionAdapterError` |
86
+
26
87
  ## Features
27
88
 
28
- - Transaction state machine
29
- - AsyncLocalStorage context propagation
30
- - Savepoints for nested transactions
89
+ - Transaction state machine with enforced transitions
90
+ - AsyncLocalStorage context propagation, with suspension
91
+ - Savepoints for nested transactions, released on commit
31
92
  - Before/after hooks
32
- - Adapter abstraction for multiple databases
33
- - Automatic rollback on errors
93
+ - Adapter abstraction with capability enforcement
94
+ - Retry with fixed or exponential backoff
95
+ - Rollback-only and timeout semantics
96
+ - Lifecycle events for observability
97
+
98
+ ## Safety Notes
99
+
100
+ - The rollback-only flag is read **before** the adapter is asked to commit, so
101
+ a transaction marked rollback-only — including one that timed out — is rolled
102
+ back and `commit()` rejects. A rollback can never be reported as a commit.
103
+ - Committing a transaction that is not active throws rather than silently
104
+ doing nothing. Only an already-committed transaction is a no-op.
105
+ - A `nested` transaction rolls back to its savepoint, never to the connection.
106
+ Savepoints are always created on the connection, including when the
107
+ enclosing scope is itself a savepoint or a participant.
108
+ - `begin()` and `run()` both honour `timeout`; completing a transaction through
109
+ `manager.commit()` / `manager.rollback()` releases its timer and registry entry.
110
+ - Failures thrown by `afterCommit` callbacks never undo the commit; they are
111
+ reported to `hooks.onError` as an `AggregateError`.
112
+ - `retry` replays only attempts that opened their own transaction. An attempt
113
+ that joined an enclosing transaction has marked it rollback-only and is not
114
+ replayed.
34
115
 
35
116
  ## Use Cases
36
117
 
37
118
  - Database transaction management
38
- - Distributed transaction coordination
39
119
  - Unit of Work pattern
40
120
  - Audit logging with transaction context
@@ -6,6 +6,10 @@
6
6
  import type { TransactionAdapter, TransactionAdapterCapabilities } from "../transactionTypes/transactionAdapter.js";
7
7
  /**
8
8
  * Create an in-memory transaction adapter (useful for testing).
9
+ *
10
+ * Capabilities are declared honestly: the adapter emulates savepoints and
11
+ * accepts every isolation level, because it enforces none of them and so
12
+ * cannot fail to provide what it claims.
9
13
  */
10
14
  export declare function createInMemoryAdapter(): TransactionAdapter;
11
15
  /**
@@ -3,36 +3,72 @@
3
3
  *
4
4
  * @module adapter/adapter
5
5
  */
6
+ import { TransactionAdapterError } from "../transactionErrors/transactionError.types.js";
6
7
  let counter = 0;
8
+ /** Narrows an opaque handle to an in-memory handle, or fails loudly. */
9
+ function asHandle(handle) {
10
+ if (typeof handle !== "object" ||
11
+ handle === null ||
12
+ !("savepoints" in handle)) {
13
+ throw new TransactionAdapterError("Handle was not issued by the in-memory adapter");
14
+ }
15
+ return handle;
16
+ }
7
17
  /**
8
18
  * Create an in-memory transaction adapter (useful for testing).
19
+ *
20
+ * Capabilities are declared honestly: the adapter emulates savepoints and
21
+ * accepts every isolation level, because it enforces none of them and so
22
+ * cannot fail to provide what it claims.
9
23
  */
10
24
  export function createInMemoryAdapter() {
11
25
  const handles = new Map();
12
26
  return {
13
27
  capabilities: Object.freeze({
14
- savepoints: false,
15
- nestedTransactions: false,
16
- isolationLevels: ["read_committed"],
17
- readOnlyTransactions: false,
18
- timeouts: false,
28
+ savepoints: true,
29
+ nestedTransactions: true,
30
+ isolationLevels: [
31
+ "read_uncommitted",
32
+ "read_committed",
33
+ "repeatable_read",
34
+ "serializable",
35
+ ],
36
+ readOnlyTransactions: true,
37
+ timeouts: true,
19
38
  }),
20
- async begin() {
39
+ async begin(_options) {
21
40
  const id = `mem_${++counter}`;
22
- const handle = { id, active: true };
41
+ const handle = { id, active: true, savepoints: [] };
23
42
  handles.set(id, handle);
24
43
  return handle;
25
44
  },
26
45
  async commit(handle) {
27
- const h = handle;
46
+ const h = asHandle(handle);
28
47
  h.active = false;
29
48
  handles.delete(h.id);
30
49
  },
31
50
  async rollback(handle) {
32
- const h = handle;
51
+ const h = asHandle(handle);
33
52
  h.active = false;
34
53
  handles.delete(h.id);
35
54
  },
55
+ async createSavepoint(handle, name) {
56
+ asHandle(handle).savepoints.push(name);
57
+ },
58
+ async rollbackToSavepoint(handle, name) {
59
+ const h = asHandle(handle);
60
+ const index = h.savepoints.indexOf(name);
61
+ if (index === -1) {
62
+ throw new TransactionAdapterError(`Unknown savepoint: ${name}`);
63
+ }
64
+ h.savepoints.length = index + 1;
65
+ },
66
+ async releaseSavepoint(handle, name) {
67
+ const h = asHandle(handle);
68
+ const index = h.savepoints.indexOf(name);
69
+ if (index !== -1)
70
+ h.savepoints.splice(index, 1);
71
+ },
36
72
  };
37
73
  }
38
74
  /**
@@ -16,6 +16,9 @@ export function createTransactionContext() {
16
16
  async run(transaction, callback) {
17
17
  return storage.run(transaction, callback);
18
18
  },
19
+ async exit(callback) {
20
+ return storage.exit(callback);
21
+ },
19
22
  };
20
23
  }
21
24
  /** Default context instance. */
package/dist/index.d.ts CHANGED
@@ -16,5 +16,5 @@ export * from "./adapter/index.js";
16
16
  export * from "./hooks/index.js";
17
17
  export * from "./registry/index.js";
18
18
  export * from "./utils/index.js";
19
- export { createTransactionManager, type TransactionManagerOptions, } from "./manager/index.js";
19
+ export { createTransactionManager, createEmitter, type TransactionEmitter, type TransactionManagerOptions, } from "./manager/index.js";
20
20
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -16,5 +16,5 @@ export * from "./adapter/index.js";
16
16
  export * from "./hooks/index.js";
17
17
  export * from "./registry/index.js";
18
18
  export * from "./utils/index.js";
19
- export { createTransactionManager, } from "./manager/index.js";
19
+ export { createTransactionManager, createEmitter, } from "./manager/index.js";
20
20
  //# sourceMappingURL=index.js.map
@@ -4,4 +4,11 @@
4
4
  * @module manager
5
5
  */
6
6
  export { createTransactionManager, type TransactionManagerOptions, } from "./manager.core.js";
7
+ export { commitTransaction, rollbackTransaction } from "./manager.commit.js";
8
+ export { resolvePropagation, suspendsTransaction, } from "./manager.propagation.js";
9
+ export type { PropagationContext } from "./manager.propagation.js";
10
+ export { assertAdapterSupports } from "./manager.capabilities.js";
11
+ export { withRetry } from "./manager.retry.js";
12
+ export { createEmitter } from "./manager.events.js";
13
+ export type { TransactionEmitter } from "./manager.events.js";
7
14
  //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,9 @@
4
4
  * @module manager
5
5
  */
6
6
  export { createTransactionManager, } from "./manager.core.js";
7
+ export { commitTransaction, rollbackTransaction } from "./manager.commit.js";
8
+ export { resolvePropagation, suspendsTransaction, } from "./manager.propagation.js";
9
+ export { assertAdapterSupports } from "./manager.capabilities.js";
10
+ export { withRetry } from "./manager.retry.js";
11
+ export { createEmitter } from "./manager.events.js";
7
12
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Adapter capability enforcement.
3
+ *
4
+ * Every adapter declares what it supports. Requesting something it does not
5
+ * provide has to fail loudly: silently running a `serializable` unit of work
6
+ * at the driver's default isolation is worse than not starting it.
7
+ *
8
+ * @module manager/manager.capabilities
9
+ */
10
+ import type { TransactionOptions } from "../transactionTypes/transaction.interface.js";
11
+ import type { TransactionAdapter } from "../transactionTypes/transactionAdapter.js";
12
+ /**
13
+ * Assert that an adapter can honour the options a transaction requests.
14
+ *
15
+ * @param adapter - The adapter about to begin the transaction.
16
+ * @param options - The requested options.
17
+ * A capability mismatch is a configuration error, not an adapter fault, so it
18
+ * no longer shares `TransactionAdapterError` with genuine driver failures:
19
+ * `TransactionIsolationError` names the level, `TransactionCapabilityError`
20
+ * names the capability.
21
+ *
22
+ * @throws {TransactionIsolationError} when the isolation level is unsupported.
23
+ * @throws {TransactionCapabilityError} when another capability is missing.
24
+ */
25
+ export declare function assertAdapterSupports(adapter: TransactionAdapter, options: TransactionOptions | undefined): void;
26
+ //# sourceMappingURL=manager.capabilities.d.ts.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Adapter capability enforcement.
3
+ *
4
+ * Every adapter declares what it supports. Requesting something it does not
5
+ * provide has to fail loudly: silently running a `serializable` unit of work
6
+ * at the driver's default isolation is worse than not starting it.
7
+ *
8
+ * @module manager/manager.capabilities
9
+ */
10
+ import { TransactionCapabilityError, TransactionIsolationError, } from "../transactionErrors/transactionError.types.js";
11
+ /**
12
+ * Assert that an adapter can honour the options a transaction requests.
13
+ *
14
+ * @param adapter - The adapter about to begin the transaction.
15
+ * @param options - The requested options.
16
+ * A capability mismatch is a configuration error, not an adapter fault, so it
17
+ * no longer shares `TransactionAdapterError` with genuine driver failures:
18
+ * `TransactionIsolationError` names the level, `TransactionCapabilityError`
19
+ * names the capability.
20
+ *
21
+ * @throws {TransactionIsolationError} when the isolation level is unsupported.
22
+ * @throws {TransactionCapabilityError} when another capability is missing.
23
+ */
24
+ export function assertAdapterSupports(adapter, options) {
25
+ if (!options)
26
+ return;
27
+ const { capabilities } = adapter;
28
+ if (options.isolation !== undefined &&
29
+ !capabilities.isolationLevels.includes(options.isolation)) {
30
+ throw new TransactionIsolationError(options.isolation);
31
+ }
32
+ if (options.readOnly === true && !capabilities.readOnlyTransactions) {
33
+ throw new TransactionCapabilityError("read-only transactions");
34
+ }
35
+ if (options.timeout !== undefined &&
36
+ options.timeout > 0 &&
37
+ !capabilities.timeouts) {
38
+ throw new TransactionCapabilityError("transaction timeouts");
39
+ }
40
+ }
41
+ //# sourceMappingURL=manager.capabilities.js.map
@@ -1,17 +1,34 @@
1
1
  /**
2
2
  * Transaction commit and rollback orchestration.
3
3
  *
4
+ * Ordering is the whole point of this module. The rollback-only flag is read
5
+ * before the adapter is asked to commit, never after: once `adapter.commit()`
6
+ * has returned there is nothing left to decide.
7
+ *
4
8
  * @module manager/manager.commit
5
9
  */
6
10
  import type { Transaction } from "../transactionTypes/transaction.interface.js";
7
11
  import type { TransactionAdapter } from "../transactionTypes/transactionAdapter.js";
8
12
  import type { TransactionHooks } from "../transactionTypes/transactionHooks.js";
13
+ import type { TransactionEmitter } from "./manager.events.js";
9
14
  /**
10
15
  * Commit a transaction with hooks and adapter coordination.
16
+ *
17
+ * A participant commits nothing — the scope that opened the transaction does
18
+ * that — and a non-transactional scope has no adapter to talk to. Anything
19
+ * else must be active: committing a transaction that was already rolled back
20
+ * is a programming error, not a no-op.
21
+ *
22
+ * @throws {TransactionTimeoutError} when the transaction outlived its timeout.
23
+ * @throws {TransactionRollbackError} when the transaction is rollback-only.
24
+ * @throws {TransactionStateError} when the transaction cannot be committed.
25
+ * @throws {TransactionCommitError} when the adapter refuses the commit.
11
26
  */
12
- export declare function commitTransaction(transaction: Transaction, adapter: TransactionAdapter, hooks?: TransactionHooks): Promise<void>;
27
+ export declare function commitTransaction(transaction: Transaction, adapter: TransactionAdapter, hooks?: TransactionHooks, emit?: TransactionEmitter): Promise<void>;
13
28
  /**
14
29
  * Rollback a transaction with hooks and adapter coordination.
30
+ *
31
+ * @throws {TransactionRollbackError} when the adapter refuses the rollback.
15
32
  */
16
- export declare function rollbackTransaction(transaction: Transaction, adapter: TransactionAdapter, reason?: unknown, hooks?: TransactionHooks): Promise<void>;
33
+ export declare function rollbackTransaction(transaction: Transaction, adapter: TransactionAdapter, reason?: unknown, hooks?: TransactionHooks, emit?: TransactionEmitter): Promise<void>;
17
34
  //# sourceMappingURL=manager.commit.d.ts.map
@@ -1,66 +1,163 @@
1
1
  /**
2
2
  * Transaction commit and rollback orchestration.
3
3
  *
4
+ * Ordering is the whole point of this module. The rollback-only flag is read
5
+ * before the adapter is asked to commit, never after: once `adapter.commit()`
6
+ * has returned there is nothing left to decide.
7
+ *
4
8
  * @module manager/manager.commit
5
9
  */
6
- import { TransactionCommitError, TransactionRollbackError, } from "../transactionErrors/transactionError.types.js";
10
+ import { asSavepointHandle, internals, } from "../transaction/transaction.internal.js";
11
+ import { canTransition } from "../transaction/transactionStateMachine.js";
12
+ import { SavepointError, TransactionCommitError, TransactionRollbackError, TransactionStateError, TransactionTimeoutError, } from "../transactionErrors/transactionError.types.js";
13
+ import { noopEmitter, TRANSACTION_EVENTS } from "./manager.events.js";
14
+ /** Moves a transaction to `failed` when the state machine allows it. */
15
+ function markFailed(transaction) {
16
+ if (canTransition(transaction.state, "failed")) {
17
+ internals(transaction)._transition("failed");
18
+ }
19
+ }
20
+ /** Releases a savepoint, or commits the connection for a root transaction. */
21
+ async function adapterCommit(transaction, adapter) {
22
+ const handle = internals(transaction)._getHandle();
23
+ const savepoint = asSavepointHandle(handle);
24
+ if (savepoint) {
25
+ if (adapter.releaseSavepoint) {
26
+ try {
27
+ await adapter.releaseSavepoint(savepoint.parent, savepoint.savepoint);
28
+ }
29
+ catch (error) {
30
+ throw new SavepointError(`Failed to release savepoint "${savepoint.savepoint}"`, error);
31
+ }
32
+ }
33
+ return;
34
+ }
35
+ await adapter.commit(handle);
36
+ }
37
+ /** Rolls back to a savepoint, or rolls back the connection. */
38
+ async function adapterRollback(transaction, adapter, reason) {
39
+ const handle = internals(transaction)._getHandle();
40
+ if (handle === undefined)
41
+ return;
42
+ const savepoint = asSavepointHandle(handle);
43
+ if (savepoint) {
44
+ try {
45
+ if (adapter.rollbackToSavepoint) {
46
+ await adapter.rollbackToSavepoint(savepoint.parent, savepoint.savepoint);
47
+ }
48
+ if (adapter.releaseSavepoint) {
49
+ await adapter.releaseSavepoint(savepoint.parent, savepoint.savepoint);
50
+ }
51
+ }
52
+ catch (error) {
53
+ throw new SavepointError(`Failed to roll back to savepoint "${savepoint.savepoint}"`, error);
54
+ }
55
+ return;
56
+ }
57
+ await adapter.rollback(handle, reason);
58
+ }
7
59
  /**
8
60
  * Commit a transaction with hooks and adapter coordination.
61
+ *
62
+ * A participant commits nothing — the scope that opened the transaction does
63
+ * that — and a non-transactional scope has no adapter to talk to. Anything
64
+ * else must be active: committing a transaction that was already rolled back
65
+ * is a programming error, not a no-op.
66
+ *
67
+ * @throws {TransactionTimeoutError} when the transaction outlived its timeout.
68
+ * @throws {TransactionRollbackError} when the transaction is rollback-only.
69
+ * @throws {TransactionStateError} when the transaction cannot be committed.
70
+ * @throws {TransactionCommitError} when the adapter refuses the commit.
9
71
  */
10
- export async function commitTransaction(transaction, adapter, hooks) {
11
- if (transaction.state !== "active")
72
+ export async function commitTransaction(transaction, adapter, hooks, emit = noopEmitter) {
73
+ if (transaction.kind === "participant")
74
+ return;
75
+ if (transaction.state === "committed")
76
+ return;
77
+ if (transaction.kind === "none") {
78
+ await transaction.commit();
12
79
  return;
13
- if (hooks?.beforeCommit) {
14
- await hooks.beforeCommit({ transaction });
15
80
  }
16
- const handle = transaction._getHandle();
81
+ if (transaction.state !== "active") {
82
+ throw new TransactionStateError(transaction.state, "commit");
83
+ }
84
+ if (transaction.isRollbackOnly()) {
85
+ const reason = internals(transaction)._getRollbackOnlyReason() ?? "marked rollback-only";
86
+ await rollbackTransaction(transaction, adapter, reason, hooks, emit);
87
+ // A timeout is a distinct failure from a caller marking the transaction
88
+ // rollback-only, and the timed-out case used to be indistinguishable
89
+ // because both raised TransactionRollbackError.
90
+ if (transaction.timedOut) {
91
+ emit(TRANSACTION_EVENTS.TIMED_OUT, transaction, reason);
92
+ throw new TransactionTimeoutError(transaction.id, transaction.options.timeout ?? 0);
93
+ }
94
+ throw new TransactionRollbackError(transaction.id, {
95
+ originalError: reason,
96
+ });
97
+ }
98
+ if (hooks?.beforeCommit)
99
+ await hooks.beforeCommit({ transaction });
100
+ emit(TRANSACTION_EVENTS.COMMITTING, transaction);
17
101
  try {
18
- await adapter.commit(handle);
102
+ await adapterCommit(transaction, adapter);
19
103
  await transaction.commit();
20
104
  }
21
105
  catch (error) {
22
- if (transaction.state === "committing") {
23
- transaction._transition("failed");
24
- }
25
- if (hooks?.onError) {
106
+ markFailed(transaction);
107
+ emit(TRANSACTION_EVENTS.FAILED, transaction, error);
108
+ if (hooks?.onError)
26
109
  await hooks.onError({ transaction, error });
27
- }
28
110
  throw new TransactionCommitError(transaction.id, error);
29
111
  }
30
- if (hooks?.afterCommit) {
31
- await hooks.afterCommit({ transaction });
112
+ emit(TRANSACTION_EVENTS.COMMITTED, transaction);
113
+ // afterCommit callbacks that threw did not undo the commit, but they
114
+ // used to fail silently. Report them without changing the outcome.
115
+ const callbackErrors = internals(transaction)._drainCallbackErrors();
116
+ if (callbackErrors.length > 0 && hooks?.onError) {
117
+ await hooks.onError({
118
+ transaction,
119
+ error: new AggregateError(callbackErrors, "after-commit callback failures"),
120
+ });
32
121
  }
122
+ if (hooks?.afterCommit)
123
+ await hooks.afterCommit({ transaction });
33
124
  }
34
125
  /**
35
126
  * Rollback a transaction with hooks and adapter coordination.
127
+ *
128
+ * @throws {TransactionRollbackError} when the adapter refuses the rollback.
36
129
  */
37
- export async function rollbackTransaction(transaction, adapter, reason, hooks) {
130
+ export async function rollbackTransaction(transaction, adapter, reason, hooks, emit = noopEmitter) {
131
+ if (transaction.kind === "participant") {
132
+ await transaction.rollback(reason);
133
+ return;
134
+ }
38
135
  if (transaction.state === "committed" ||
39
136
  transaction.state === "rolled_back" ||
40
137
  transaction.state === "failed") {
41
138
  return;
42
139
  }
43
- if (hooks?.beforeRollback) {
140
+ if (hooks?.beforeRollback)
44
141
  await hooks.beforeRollback({ transaction });
45
- }
46
- const handle = transaction._getHandle();
142
+ emit(TRANSACTION_EVENTS.ROLLING_BACK, transaction, reason);
47
143
  try {
48
- if (handle !== undefined) {
49
- await adapter.rollback(handle, reason);
144
+ if (transaction.kind !== "none") {
145
+ await adapterRollback(transaction, adapter, reason);
50
146
  }
51
147
  await transaction.rollback(reason);
52
148
  }
53
149
  catch (error) {
54
- if (hooks?.onError) {
150
+ markFailed(transaction);
151
+ emit(TRANSACTION_EVENTS.FAILED, transaction, error);
152
+ if (hooks?.onError)
55
153
  await hooks.onError({ transaction, error });
56
- }
57
154
  throw new TransactionRollbackError(transaction.id, {
58
155
  cause: error,
59
156
  originalError: reason,
60
157
  });
61
158
  }
62
- if (hooks?.afterRollback) {
159
+ emit(TRANSACTION_EVENTS.ROLLED_BACK, transaction, reason);
160
+ if (hooks?.afterRollback)
63
161
  await hooks.afterRollback({ transaction });
64
- }
65
162
  }
66
163
  //# sourceMappingURL=manager.commit.js.map
@@ -4,23 +4,54 @@
4
4
  * @module manager/manager
5
5
  */
6
6
  import type { Transaction, TransactionOptions } from "../transactionTypes/transaction.interface.js";
7
- import type { TransactionAdapter } from "../transactionTypes/transactionAdapter.js";
8
- import type { TransactionContext } from "../transactionTypes/transactionAdapter.js";
9
- import type { TransactionHooks } from "../transactionTypes/transactionHooks.js";
7
+ import type { TransactionAdapter, TransactionContext } from "../transactionTypes/transactionAdapter.js";
8
+ import type { TransactionEventHandler, TransactionHooks, TransactionRegistry } from "../transactionTypes/transactionHooks.js";
10
9
  /** Options for creating a transaction manager. */
11
10
  export interface TransactionManagerOptions {
12
11
  readonly adapter: TransactionAdapter;
13
12
  readonly context?: TransactionContext;
14
13
  readonly hooks?: TransactionHooks;
14
+ /** Optional registry notified as transactions start and finish. */
15
+ readonly registry?: TransactionRegistry;
16
+ /**
17
+ * Optional observer for transaction lifecycle events.
18
+ *
19
+ * Receives a {@link TransactionEvent} for each of `TRANSACTION_EVENTS`.
20
+ * A throwing observer is ignored rather than failing the transaction.
21
+ */
22
+ readonly onEvent?: TransactionEventHandler;
15
23
  }
16
24
  /**
17
25
  * Create a transaction manager.
18
26
  */
19
27
  export declare function createTransactionManager(options: TransactionManagerOptions): {
28
+ /**
29
+ * Begin a transaction, applying the requested propagation mode.
30
+ *
31
+ * With the default `required` propagation inside an existing transaction
32
+ * this returns a participant: a handle that observes the enclosing
33
+ * transaction but never commits it.
34
+ */
20
35
  begin(opts?: TransactionOptions): Promise<Transaction>;
36
+ /**
37
+ * Run a callback inside a transaction, committing or rolling back around it.
38
+ *
39
+ * Only a transaction this call opened is completed here: joining an
40
+ * enclosing transaction must not commit it, and a failure inside a
41
+ * participant marks the enclosing transaction rollback-only instead.
42
+ */
21
43
  run<T>(callback: (transaction: Transaction) => Promise<T>, opts?: TransactionOptions): Promise<T>;
44
+ /**
45
+ * Commit a transaction. Participants and committed transactions are no-ops.
46
+ *
47
+ * Completing a transaction opened with `begin()` also releases its
48
+ * timeout timer and registry entry; both used to be released only by
49
+ * `run()`, so hand-managed transactions stayed in the registry forever.
50
+ */
22
51
  commit(transaction: Transaction): Promise<void>;
52
+ /** Roll back a transaction, or mark the joined transaction rollback-only. */
23
53
  rollback(transaction: Transaction, reason?: unknown): Promise<void>;
54
+ /** The transaction in scope for the current async execution, if any. */
24
55
  getCurrent(): Transaction | undefined;
25
56
  };
26
57
  //# sourceMappingURL=manager.core.d.ts.map