@zudojs/transactions 0.0.1 → 1.0.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 (91) hide show
  1. package/README.md +82 -11
  2. package/dist/adapter/adapter.core.d.ts +4 -0
  3. package/dist/adapter/adapter.core.js +45 -9
  4. package/dist/context/context.core.js +3 -0
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/manager/index.d.ts +7 -0
  8. package/dist/manager/index.js +5 -0
  9. package/dist/manager/manager.capabilities.d.ts +26 -0
  10. package/dist/manager/manager.capabilities.js +41 -0
  11. package/dist/manager/manager.commit.d.ts +19 -2
  12. package/dist/manager/manager.commit.js +112 -24
  13. package/dist/manager/manager.core.d.ts +28 -3
  14. package/dist/manager/manager.core.js +75 -53
  15. package/dist/manager/manager.events.d.ts +31 -0
  16. package/dist/manager/manager.events.js +45 -0
  17. package/dist/manager/manager.propagation.d.ts +33 -5
  18. package/dist/manager/manager.propagation.js +100 -36
  19. package/dist/manager/manager.retry.d.ts +20 -0
  20. package/dist/manager/manager.retry.js +46 -0
  21. package/dist/transaction/index.d.ts +5 -3
  22. package/dist/transaction/index.js +4 -3
  23. package/dist/transaction/transaction.core.d.ts +6 -1
  24. package/dist/transaction/transaction.core.js +60 -57
  25. package/dist/transaction/transaction.internal.d.ts +65 -0
  26. package/dist/transaction/transaction.internal.js +66 -0
  27. package/dist/transaction/transaction.participant.d.ts +31 -0
  28. package/dist/transaction/transaction.participant.js +81 -0
  29. package/dist/transaction/transactionStateMachine.d.ts +10 -0
  30. package/dist/transaction/transactionStateMachine.js +16 -2
  31. package/dist/transactionTypes/index.d.ts +4 -4
  32. package/dist/transactionTypes/transaction.interface.d.ts +19 -1
  33. package/dist/transactionTypes/transactionAdapter.d.ts +7 -9
  34. package/dist/transactionTypes/transactionHooks.d.ts +0 -7
  35. package/dist/transactionTypes/transactionState.d.ts +9 -0
  36. package/dist/utils/utils.helper.d.ts +6 -1
  37. package/dist/utils/utils.helper.js +8 -2
  38. package/package.json +13 -6
  39. package/dist/.tsbuildinfo +0 -1
  40. package/dist/adapter/adapter.core.d.ts.map +0 -1
  41. package/dist/adapter/adapter.core.js.map +0 -1
  42. package/dist/adapter/index.d.ts.map +0 -1
  43. package/dist/adapter/index.js.map +0 -1
  44. package/dist/context/context.core.d.ts.map +0 -1
  45. package/dist/context/context.core.js.map +0 -1
  46. package/dist/context/index.d.ts.map +0 -1
  47. package/dist/context/index.js.map +0 -1
  48. package/dist/hooks/hooks.core.d.ts.map +0 -1
  49. package/dist/hooks/hooks.core.js.map +0 -1
  50. package/dist/hooks/index.d.ts.map +0 -1
  51. package/dist/hooks/index.js.map +0 -1
  52. package/dist/index.d.ts.map +0 -1
  53. package/dist/index.js.map +0 -1
  54. package/dist/manager/index.d.ts.map +0 -1
  55. package/dist/manager/index.js.map +0 -1
  56. package/dist/manager/manager.commit.d.ts.map +0 -1
  57. package/dist/manager/manager.commit.js.map +0 -1
  58. package/dist/manager/manager.core.d.ts.map +0 -1
  59. package/dist/manager/manager.core.js.map +0 -1
  60. package/dist/manager/manager.propagation.d.ts.map +0 -1
  61. package/dist/manager/manager.propagation.js.map +0 -1
  62. package/dist/registry/index.d.ts.map +0 -1
  63. package/dist/registry/index.js.map +0 -1
  64. package/dist/registry/registry.core.d.ts.map +0 -1
  65. package/dist/registry/registry.core.js.map +0 -1
  66. package/dist/transaction/index.d.ts.map +0 -1
  67. package/dist/transaction/index.js.map +0 -1
  68. package/dist/transaction/transaction.core.d.ts.map +0 -1
  69. package/dist/transaction/transaction.core.js.map +0 -1
  70. package/dist/transaction/transactionStateMachine.d.ts.map +0 -1
  71. package/dist/transaction/transactionStateMachine.js.map +0 -1
  72. package/dist/transactionErrors/index.d.ts.map +0 -1
  73. package/dist/transactionErrors/index.js.map +0 -1
  74. package/dist/transactionErrors/transactionError.base.d.ts.map +0 -1
  75. package/dist/transactionErrors/transactionError.base.js.map +0 -1
  76. package/dist/transactionErrors/transactionError.types.d.ts.map +0 -1
  77. package/dist/transactionErrors/transactionError.types.js.map +0 -1
  78. package/dist/transactionTypes/index.d.ts.map +0 -1
  79. package/dist/transactionTypes/index.js.map +0 -1
  80. package/dist/transactionTypes/transaction.interface.d.ts.map +0 -1
  81. package/dist/transactionTypes/transaction.interface.js.map +0 -1
  82. package/dist/transactionTypes/transactionAdapter.d.ts.map +0 -1
  83. package/dist/transactionTypes/transactionAdapter.js.map +0 -1
  84. package/dist/transactionTypes/transactionHooks.d.ts.map +0 -1
  85. package/dist/transactionTypes/transactionHooks.js.map +0 -1
  86. package/dist/transactionTypes/transactionState.d.ts.map +0 -1
  87. package/dist/transactionTypes/transactionState.js.map +0 -1
  88. package/dist/utils/index.d.ts.map +0 -1
  89. package/dist/utils/index.js.map +0 -1
  90. package/dist/utils/utils.helper.d.ts.map +0 -1
  91. package/dist/utils/utils.helper.js.map +0 -1
package/README.md CHANGED
@@ -13,28 +13,99 @@ 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.
34
106
 
35
107
  ## Use Cases
36
108
 
37
109
  - Database transaction management
38
- - Distributed transaction coordination
39
110
  - Unit of Work pattern
40
111
  - 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,154 @@
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")
12
74
  return;
13
- if (hooks?.beforeCommit) {
14
- await hooks.beforeCommit({ transaction });
75
+ if (transaction.state === "committed")
76
+ return;
77
+ if (transaction.kind === "none") {
78
+ await transaction.commit();
79
+ return;
80
+ }
81
+ if (transaction.state !== "active") {
82
+ throw new TransactionStateError(transaction.state, "commit");
15
83
  }
16
- const handle = transaction._getHandle();
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) {
112
+ emit(TRANSACTION_EVENTS.COMMITTED, transaction);
113
+ if (hooks?.afterCommit)
31
114
  await hooks.afterCommit({ transaction });
32
- }
33
115
  }
34
116
  /**
35
117
  * Rollback a transaction with hooks and adapter coordination.
118
+ *
119
+ * @throws {TransactionRollbackError} when the adapter refuses the rollback.
36
120
  */
37
- export async function rollbackTransaction(transaction, adapter, reason, hooks) {
121
+ export async function rollbackTransaction(transaction, adapter, reason, hooks, emit = noopEmitter) {
122
+ if (transaction.kind === "participant") {
123
+ await transaction.rollback(reason);
124
+ return;
125
+ }
38
126
  if (transaction.state === "committed" ||
39
127
  transaction.state === "rolled_back" ||
40
128
  transaction.state === "failed") {
41
129
  return;
42
130
  }
43
- if (hooks?.beforeRollback) {
131
+ if (hooks?.beforeRollback)
44
132
  await hooks.beforeRollback({ transaction });
45
- }
46
- const handle = transaction._getHandle();
133
+ emit(TRANSACTION_EVENTS.ROLLING_BACK, transaction, reason);
47
134
  try {
48
- if (handle !== undefined) {
49
- await adapter.rollback(handle, reason);
135
+ if (transaction.kind !== "none") {
136
+ await adapterRollback(transaction, adapter, reason);
50
137
  }
51
138
  await transaction.rollback(reason);
52
139
  }
53
140
  catch (error) {
54
- if (hooks?.onError) {
141
+ markFailed(transaction);
142
+ emit(TRANSACTION_EVENTS.FAILED, transaction, error);
143
+ if (hooks?.onError)
55
144
  await hooks.onError({ transaction, error });
56
- }
57
145
  throw new TransactionRollbackError(transaction.id, {
58
146
  cause: error,
59
147
  originalError: reason,
60
148
  });
61
149
  }
62
- if (hooks?.afterRollback) {
150
+ emit(TRANSACTION_EVENTS.ROLLED_BACK, transaction, reason);
151
+ if (hooks?.afterRollback)
63
152
  await hooks.afterRollback({ transaction });
64
- }
65
153
  }
66
154
  //# sourceMappingURL=manager.commit.js.map
@@ -4,23 +4,48 @@
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
+ /** Commit a transaction. Participants and committed transactions are no-ops. */
22
45
  commit(transaction: Transaction): Promise<void>;
46
+ /** Roll back a transaction, or mark the joined transaction rollback-only. */
23
47
  rollback(transaction: Transaction, reason?: unknown): Promise<void>;
48
+ /** The transaction in scope for the current async execution, if any. */
24
49
  getCurrent(): Transaction | undefined;
25
50
  };
26
51
  //# sourceMappingURL=manager.core.d.ts.map