@zudojs/transactions 1.1.1 → 1.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
@@ -47,6 +47,47 @@ await manager.run(handler, {
47
47
  });
48
48
  ```
49
49
 
50
+ ## Reaching the adapter handle
51
+
52
+ Whatever your adapter's `begin()` returned (typically a database client or
53
+ connection bound to the transaction) is available inside the transaction:
54
+
55
+ ```typescript
56
+ import { currentTransactionHandle, getTransactionHandle } from "@zudojs/transactions";
57
+
58
+ await manager.run(async (transaction) => {
59
+ const tx = getTransactionHandle<PoolClient>(transaction);
60
+ // or, anywhere below this call in the same async flow:
61
+ const same = manager.getCurrentHandle<PoolClient>();
62
+ const alsoSame = currentTransactionHandle<PoolClient>(); // default context only
63
+ await tx!.query("INSERT ...");
64
+ });
65
+ ```
66
+
67
+ A savepoint resolves to its connection's handle and a participant to the
68
+ joined transaction's. Outside a transaction, or in a non-transactional scope
69
+ (`supports`/`not_supported`/`never` with nothing to join), the result is
70
+ `undefined`. Use the handle to issue work; commit and roll back through the
71
+ manager, not the handle.
72
+
73
+ ## Timeouts
74
+
75
+ With `timeout`, the transaction's `signal` aborts when it runs out, with a
76
+ `TransactionTimeoutError` as `signal.reason`. `run()` then stops waiting for
77
+ the callback, rolls back, and rejects with that error. JavaScript cannot stop
78
+ the callback itself, so pass the signal to anything cancellable:
79
+
80
+ ```typescript
81
+ await manager.run(
82
+ async (transaction) => {
83
+ await fetch(url, { signal: transaction.signal });
84
+ },
85
+ { timeout: 5_000 },
86
+ );
87
+ ```
88
+
89
+ `timed_out` is emitted once, when the timeout fires.
90
+
50
91
  ## Propagation
51
92
 
52
93
  | Mode | Transaction in progress | None in progress |
@@ -81,7 +122,8 @@ the transaction.
81
122
  | Condition | Error |
82
123
  | -------------------------------------- | ------------------------------ |
83
124
  | Transaction outlived its `timeout` | `TransactionTimeoutError` |
84
- | Marked rollback-only, then committed | `TransactionRollbackError` |
125
+ | Marked rollback-only, then committed | `TransactionRollbackOnlyError` (a `TransactionRollbackError`): "commit refused: transaction marked rollback-only" |
126
+ | Rolling back a committed transaction | `TransactionStateError` |
85
127
  | Adapter lacks the requested isolation | `TransactionIsolationError` |
86
128
  | Adapter lacks another requested feature | `TransactionCapabilityError` |
87
129
  | Savepoint create/rollback/release fails | `SavepointError` |
@@ -108,6 +150,9 @@ the transaction.
108
150
  back and `commit()` rejects. A rollback can never be reported as a commit.
109
151
  - Committing a transaction that is not active throws rather than silently
110
152
  doing nothing. Only an already-committed transaction is a no-op.
153
+ - Rolling back a committed transaction throws `TransactionStateError`: the
154
+ commit cannot be undone, and pretending otherwise hid bugs. Rolling back a
155
+ transaction that is already rolled back (or failed) is still a no-op.
111
156
  - A `nested` transaction rolls back to its savepoint, never to the connection.
112
157
  Savepoints are always created on the connection, including when the
113
158
  enclosing scope is itself a savepoint or a participant.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Public, read-only access to a transaction's adapter handle.
3
+ *
4
+ * @module context/context.handle
5
+ */
6
+ import type { Transaction } from "../transactionTypes/transaction.interface.js";
7
+ import type { TransactionContext } from "../transactionTypes/transactionAdapter.js";
8
+ /**
9
+ * The adapter handle a transaction runs on: whatever the adapter's
10
+ * `begin()` returned (for example a database client or connection bound to
11
+ * the transaction).
12
+ *
13
+ * A savepoint resolves to its connection's handle and a participant to the
14
+ * handle of the transaction it joined. A non-transactional scope
15
+ * (`supports`, `not_supported`, `never`) has none and yields `undefined`.
16
+ *
17
+ * The handle is for issuing work inside the transaction. Committing or
18
+ * rolling back through it directly bypasses the manager's state machine,
19
+ * hooks and events; use the manager for that.
20
+ *
21
+ * @typeParam THandle - The handle type your adapter's `begin()` returns.
22
+ * @throws {TypeError} when the transaction was not created by this package.
23
+ */
24
+ export declare function getTransactionHandle<THandle = unknown>(transaction: Transaction): THandle | undefined;
25
+ /**
26
+ * The adapter handle of the transaction in scope for the current async
27
+ * execution, or `undefined` outside a transaction.
28
+ *
29
+ * Uses the default context unless one is supplied; pass the same context
30
+ * the manager was created with when it was given a custom one (or call the
31
+ * manager's `getCurrentHandle()`).
32
+ *
33
+ * @typeParam THandle - The handle type your adapter's `begin()` returns.
34
+ */
35
+ export declare function currentTransactionHandle<THandle = unknown>(context?: TransactionContext): THandle | undefined;
36
+ //# sourceMappingURL=context.handle.d.ts.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Public, read-only access to a transaction's adapter handle.
3
+ *
4
+ * @module context/context.handle
5
+ */
6
+ import { connectionHandle } from "../transaction/transaction.internal.js";
7
+ import { getDefaultContext } from "./context.core.js";
8
+ /**
9
+ * The adapter handle a transaction runs on: whatever the adapter's
10
+ * `begin()` returned (for example a database client or connection bound to
11
+ * the transaction).
12
+ *
13
+ * A savepoint resolves to its connection's handle and a participant to the
14
+ * handle of the transaction it joined. A non-transactional scope
15
+ * (`supports`, `not_supported`, `never`) has none and yields `undefined`.
16
+ *
17
+ * The handle is for issuing work inside the transaction. Committing or
18
+ * rolling back through it directly bypasses the manager's state machine,
19
+ * hooks and events; use the manager for that.
20
+ *
21
+ * @typeParam THandle - The handle type your adapter's `begin()` returns.
22
+ * @throws {TypeError} when the transaction was not created by this package.
23
+ */
24
+ export function getTransactionHandle(transaction) {
25
+ return connectionHandle(transaction);
26
+ }
27
+ /**
28
+ * The adapter handle of the transaction in scope for the current async
29
+ * execution, or `undefined` outside a transaction.
30
+ *
31
+ * Uses the default context unless one is supplied; pass the same context
32
+ * the manager was created with when it was given a custom one (or call the
33
+ * manager's `getCurrentHandle()`).
34
+ *
35
+ * @typeParam THandle - The handle type your adapter's `begin()` returns.
36
+ */
37
+ export function currentTransactionHandle(context = getDefaultContext()) {
38
+ const transaction = context.get();
39
+ return transaction ? getTransactionHandle(transaction) : undefined;
40
+ }
41
+ //# sourceMappingURL=context.handle.js.map
@@ -4,4 +4,5 @@
4
4
  * @module context
5
5
  */
6
6
  export { createTransactionContext, getDefaultContext, resetDefaultContext, } from "./context.core.js";
7
+ export { getTransactionHandle, currentTransactionHandle, } from "./context.handle.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,5 @@
4
4
  * @module context
5
5
  */
6
6
  export { createTransactionContext, getDefaultContext, resetDefaultContext, } from "./context.core.js";
7
+ export { getTransactionHandle, currentTransactionHandle, } from "./context.handle.js";
7
8
  //# sourceMappingURL=index.js.map
@@ -20,7 +20,9 @@ import type { TransactionEmitter } from "./manager.events.js";
20
20
  * is a programming error, not a no-op.
21
21
  *
22
22
  * @throws {TransactionTimeoutError} when the transaction outlived its timeout.
23
- * @throws {TransactionRollbackError} when the transaction is rollback-only.
23
+ * @throws {TransactionRollbackOnlyError} when the transaction is rollback-only
24
+ * (a `TransactionRollbackError` subclass): the commit was refused and the
25
+ * transaction rolled back.
24
26
  * @throws {TransactionStateError} when the transaction cannot be committed.
25
27
  * @throws {TransactionCommitError} when the adapter refuses the commit.
26
28
  */
@@ -28,6 +30,12 @@ export declare function commitTransaction(transaction: Transaction, adapter: Tra
28
30
  /**
29
31
  * Rollback a transaction with hooks and adapter coordination.
30
32
  *
33
+ * Rolling back a transaction that is already rolled back or failed is a
34
+ * no-op; a committed transaction cannot be undone, so asking to roll it
35
+ * back is a programming error (the same rule `Transaction.rollback()`
36
+ * enforces) rather than a silent no-op.
37
+ *
38
+ * @throws {TransactionStateError} when the transaction is already committed.
31
39
  * @throws {TransactionRollbackError} when the adapter refuses the rollback.
32
40
  */
33
41
  export declare function rollbackTransaction(transaction: Transaction, adapter: TransactionAdapter, reason?: unknown, hooks?: TransactionHooks, emit?: TransactionEmitter): Promise<void>;
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { asSavepointHandle, internals, } from "../transaction/transaction.internal.js";
11
11
  import { canTransition } from "../transaction/transactionStateMachine.js";
12
- import { SavepointError, TransactionCommitError, TransactionRollbackError, TransactionStateError, TransactionTimeoutError, } from "../transactionErrors/transactionError.types.js";
12
+ import { SavepointError, TransactionCommitError, TransactionRollbackError, TransactionRollbackOnlyError, TransactionStateError, TransactionTimeoutError, } from "../transactionErrors/transactionError.types.js";
13
13
  import { noopEmitter, TRANSACTION_EVENTS } from "./manager.events.js";
14
14
  /** Moves a transaction to `failed` when the state machine allows it. */
15
15
  function markFailed(transaction) {
@@ -65,7 +65,9 @@ async function adapterRollback(transaction, adapter, reason) {
65
65
  * is a programming error, not a no-op.
66
66
  *
67
67
  * @throws {TransactionTimeoutError} when the transaction outlived its timeout.
68
- * @throws {TransactionRollbackError} when the transaction is rollback-only.
68
+ * @throws {TransactionRollbackOnlyError} when the transaction is rollback-only
69
+ * (a `TransactionRollbackError` subclass): the commit was refused and the
70
+ * transaction rolled back.
69
71
  * @throws {TransactionStateError} when the transaction cannot be committed.
70
72
  * @throws {TransactionCommitError} when the adapter refuses the commit.
71
73
  */
@@ -85,15 +87,12 @@ export async function commitTransaction(transaction, adapter, hooks, emit = noop
85
87
  const reason = internals(transaction)._getRollbackOnlyReason() ?? "marked rollback-only";
86
88
  await rollbackTransaction(transaction, adapter, reason, hooks, emit);
87
89
  // 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
+ // rollback-only. `timed_out` was already emitted by the timer when the
91
+ // timeout fired; emitting it again here reported one timeout twice.
90
92
  if (transaction.timedOut) {
91
- emit(TRANSACTION_EVENTS.TIMED_OUT, transaction, reason);
92
93
  throw new TransactionTimeoutError(transaction.id, transaction.options.timeout ?? 0);
93
94
  }
94
- throw new TransactionRollbackError(transaction.id, {
95
- originalError: reason,
96
- });
95
+ throw new TransactionRollbackOnlyError(transaction.id, reason);
97
96
  }
98
97
  if (hooks?.beforeCommit)
99
98
  await hooks.beforeCommit({ transaction });
@@ -133,6 +132,12 @@ export async function commitTransaction(transaction, adapter, hooks, emit = noop
133
132
  /**
134
133
  * Rollback a transaction with hooks and adapter coordination.
135
134
  *
135
+ * Rolling back a transaction that is already rolled back or failed is a
136
+ * no-op; a committed transaction cannot be undone, so asking to roll it
137
+ * back is a programming error (the same rule `Transaction.rollback()`
138
+ * enforces) rather than a silent no-op.
139
+ *
140
+ * @throws {TransactionStateError} when the transaction is already committed.
136
141
  * @throws {TransactionRollbackError} when the adapter refuses the rollback.
137
142
  */
138
143
  export async function rollbackTransaction(transaction, adapter, reason, hooks, emit = noopEmitter) {
@@ -140,9 +145,10 @@ export async function rollbackTransaction(transaction, adapter, reason, hooks, e
140
145
  await transaction.rollback(reason);
141
146
  return;
142
147
  }
143
- if (transaction.state === "committed" ||
144
- transaction.state === "rolled_back" ||
145
- transaction.state === "failed") {
148
+ if (transaction.state === "committed") {
149
+ throw new TransactionStateError(transaction.state, "rollback");
150
+ }
151
+ if (transaction.state === "rolled_back" || transaction.state === "failed") {
146
152
  return;
147
153
  }
148
154
  if (hooks?.beforeRollback)
@@ -39,6 +39,12 @@ export declare function createTransactionManager(options: TransactionManagerOpti
39
39
  * Only a transaction this call opened is completed here: joining an
40
40
  * enclosing transaction must not commit it, and a failure inside a
41
41
  * participant marks the enclosing transaction rollback-only instead.
42
+ *
43
+ * When the transaction times out, `transaction.signal` aborts and this
44
+ * stops waiting for the callback: the transaction is rolled back and
45
+ * the call rejects with `TransactionTimeoutError`. An error raised
46
+ * after a successful commit (for example by an `afterCommit` hook) is
47
+ * rethrown without attempting a rollback.
42
48
  */
43
49
  run<T>(callback: (transaction: Transaction) => Promise<T>, opts?: TransactionOptions): Promise<T>;
44
50
  /**
@@ -53,5 +59,11 @@ export declare function createTransactionManager(options: TransactionManagerOpti
53
59
  rollback(transaction: Transaction, reason?: unknown): Promise<void>;
54
60
  /** The transaction in scope for the current async execution, if any. */
55
61
  getCurrent(): Transaction | undefined;
62
+ /**
63
+ * The adapter handle (what `adapter.begin()` returned) of the
64
+ * transaction in scope for the current async execution, or `undefined`
65
+ * outside a transaction. See `getTransactionHandle`.
66
+ */
67
+ getCurrentHandle<THandle = unknown>(): THandle | undefined;
56
68
  };
57
69
  //# sourceMappingURL=manager.core.d.ts.map
@@ -7,6 +7,8 @@ import { getDefaultContext } from "../context/context.core.js";
7
7
  import { internals } from "../transaction/transaction.internal.js";
8
8
  import { isTerminal } from "../transaction/transactionStateMachine.js";
9
9
  import { TransactionRollbackError } from "../transactionErrors/transactionError.types.js";
10
+ import { getTransactionHandle } from "../context/context.handle.js";
11
+ import { raceSignal } from "../utils/utils.signal.js";
10
12
  import { commitTransaction, rollbackTransaction } from "./manager.commit.js";
11
13
  import { resolvePropagation, suspendsTransaction, } from "./manager.propagation.js";
12
14
  import { withRetry } from "./manager.retry.js";
@@ -90,6 +92,12 @@ export function createTransactionManager(options) {
90
92
  * Only a transaction this call opened is completed here: joining an
91
93
  * enclosing transaction must not commit it, and a failure inside a
92
94
  * participant marks the enclosing transaction rollback-only instead.
95
+ *
96
+ * When the transaction times out, `transaction.signal` aborts and this
97
+ * stops waiting for the callback: the transaction is rolled back and
98
+ * the call rejects with `TransactionTimeoutError`. An error raised
99
+ * after a successful commit (for example by an `afterCommit` hook) is
100
+ * rethrown without attempting a rollback.
93
101
  */
94
102
  async run(callback, opts) {
95
103
  // A failed attempt that only JOINED an enclosing transaction has not
@@ -109,11 +117,13 @@ export function createTransactionManager(options) {
109
117
  joined = transaction.kind === "participant";
110
118
  const body = async () => {
111
119
  try {
112
- const result = await callback(transaction);
120
+ const result = await raceSignal(callback(transaction), transaction.signal);
113
121
  await this.commit(transaction);
114
122
  return result;
115
123
  }
116
124
  catch (error) {
125
+ if (transaction.state === "committed")
126
+ throw error;
117
127
  try {
118
128
  await this.rollback(transaction, error);
119
129
  }
@@ -166,6 +176,15 @@ export function createTransactionManager(options) {
166
176
  getCurrent() {
167
177
  return context.get();
168
178
  },
179
+ /**
180
+ * The adapter handle (what `adapter.begin()` returned) of the
181
+ * transaction in scope for the current async execution, or `undefined`
182
+ * outside a transaction. See `getTransactionHandle`.
183
+ */
184
+ getCurrentHandle() {
185
+ const current = context.get();
186
+ return current ? getTransactionHandle(current) : undefined;
187
+ },
169
188
  };
170
189
  }
171
190
  //# sourceMappingURL=manager.core.js.map
@@ -2,7 +2,7 @@
2
2
  * Core Transaction implementation with state machine enforcement.
3
3
  */
4
4
  import { randomBytes } from "node:crypto";
5
- import { TransactionRollbackError, TransactionStateError, } from "../transactionErrors/transactionError.types.js";
5
+ import { TransactionRollbackError, TransactionRollbackOnlyError, TransactionStateError, TransactionTimeoutError, } from "../transactionErrors/transactionError.types.js";
6
6
  import { canTransition, createTransitionFunction, } from "./transactionStateMachine.js";
7
7
  import { attachInternals } from "./transaction.internal.js";
8
8
  import { deferCallbacksToParent } from "./transaction.savepoint.js";
@@ -43,6 +43,10 @@ export function createTransaction(options = {}, parentId, kind = "root", parent)
43
43
  const afterRollbackCallbacks = [];
44
44
  let callbackErrors = [];
45
45
  let handle;
46
+ const controller = new AbortController();
47
+ const signal = parent
48
+ ? AbortSignal.any([controller.signal, parent.signal])
49
+ : controller.signal;
46
50
  const transition = createTransitionFunction(() => state, (next) => {
47
51
  state = next;
48
52
  });
@@ -75,6 +79,9 @@ export function createTransaction(options = {}, parentId, kind = "root", parent)
75
79
  get timedOut() {
76
80
  return timedOut;
77
81
  },
82
+ get signal() {
83
+ return signal;
84
+ },
78
85
  /**
79
86
  * Mark the transaction committed and run its after-commit callbacks.
80
87
  *
@@ -88,9 +95,7 @@ export function createTransaction(options = {}, parentId, kind = "root", parent)
88
95
  throw new TransactionStateError(state, "commit");
89
96
  }
90
97
  if (rollbackOnly) {
91
- throw new TransactionRollbackError(id, {
92
- originalError: rollbackOnlyReason ?? "marked rollback-only",
93
- });
98
+ throw new TransactionRollbackOnlyError(id, rollbackOnlyReason);
94
99
  }
95
100
  transition("committing");
96
101
  transition("committed");
@@ -145,6 +150,7 @@ export function createTransaction(options = {}, parentId, kind = "root", parent)
145
150
  _transition: transition,
146
151
  _markTimedOut: () => {
147
152
  timedOut = true;
153
+ controller.abort(new TransactionTimeoutError(id, frozenOptions.timeout ?? 0));
148
154
  },
149
155
  _getRollbackOnlyReason: () => rollbackOnlyReason,
150
156
  _drainCallbackErrors: () => callbackErrors.splice(0),
@@ -43,6 +43,9 @@ export function createParticipant(parent) {
43
43
  get timedOut() {
44
44
  return parent.timedOut;
45
45
  },
46
+ get signal() {
47
+ return parent.signal;
48
+ },
46
49
  /** No-op: the transaction is committed by whoever opened it. */
47
50
  async commit() { },
48
51
  /** Marks the joined transaction rollback-only. */
@@ -2,5 +2,5 @@
2
2
  * Transaction error types.
3
3
  */
4
4
  export { TransactionError, type TransactionErrorOptions, } from "./transactionError.base.js";
5
- export { TransactionStateError, TransactionTimeoutError, TransactionCommitError, TransactionRollbackError, TransactionAdapterError, TransactionPropagationError, TransactionIsolationError, SavepointError, TransactionRequiredError, TransactionUnexpectedError, TransactionCapabilityError, } from "./transactionError.types.js";
5
+ export { TransactionStateError, TransactionTimeoutError, TransactionCommitError, TransactionRollbackError, TransactionRollbackOnlyError, TransactionAdapterError, TransactionPropagationError, TransactionIsolationError, SavepointError, TransactionRequiredError, TransactionUnexpectedError, TransactionCapabilityError, } from "./transactionError.types.js";
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -2,5 +2,5 @@
2
2
  * Transaction error types.
3
3
  */
4
4
  export { TransactionError, } from "./transactionError.base.js";
5
- export { TransactionStateError, TransactionTimeoutError, TransactionCommitError, TransactionRollbackError, TransactionAdapterError, TransactionPropagationError, TransactionIsolationError, SavepointError, TransactionRequiredError, TransactionUnexpectedError, TransactionCapabilityError, } from "./transactionError.types.js";
5
+ export { TransactionStateError, TransactionTimeoutError, TransactionCommitError, TransactionRollbackError, TransactionRollbackOnlyError, TransactionAdapterError, TransactionPropagationError, TransactionIsolationError, SavepointError, TransactionRequiredError, TransactionUnexpectedError, TransactionCapabilityError, } from "./transactionError.types.js";
6
6
  //# sourceMappingURL=index.js.map
@@ -5,5 +5,5 @@
5
5
  * same names, constructor signatures and codes, so `instanceof` checks match
6
6
  * across both import paths.
7
7
  */
8
- export { TransactionStateError, TransactionTimeoutError, TransactionCommitError, TransactionRollbackError, TransactionAdapterError, TransactionPropagationError, TransactionIsolationError, SavepointError, TransactionRequiredError, TransactionUnexpectedError, TransactionCapabilityError, } from "@zudojs/errors";
8
+ export { TransactionStateError, TransactionTimeoutError, TransactionCommitError, TransactionRollbackError, TransactionRollbackOnlyError, TransactionAdapterError, TransactionPropagationError, TransactionIsolationError, SavepointError, TransactionRequiredError, TransactionUnexpectedError, TransactionCapabilityError, } from "@zudojs/errors";
9
9
  //# sourceMappingURL=transactionError.types.d.ts.map
@@ -5,5 +5,5 @@
5
5
  * same names, constructor signatures and codes, so `instanceof` checks match
6
6
  * across both import paths.
7
7
  */
8
- export { TransactionStateError, TransactionTimeoutError, TransactionCommitError, TransactionRollbackError, TransactionAdapterError, TransactionPropagationError, TransactionIsolationError, SavepointError, TransactionRequiredError, TransactionUnexpectedError, TransactionCapabilityError, } from "@zudojs/errors";
8
+ export { TransactionStateError, TransactionTimeoutError, TransactionCommitError, TransactionRollbackError, TransactionRollbackOnlyError, TransactionAdapterError, TransactionPropagationError, TransactionIsolationError, SavepointError, TransactionRequiredError, TransactionUnexpectedError, TransactionCapabilityError, } from "@zudojs/errors";
9
9
  //# sourceMappingURL=transactionError.types.js.map
@@ -64,9 +64,23 @@ export interface Transaction {
64
64
  readonly metadata: ReadonlyMap<string, unknown>;
65
65
  /** Whether a timeout was detected. */
66
66
  readonly timedOut: boolean;
67
+ /**
68
+ * Aborted when the transaction outlives its `timeout`, with a
69
+ * `TransactionTimeoutError` as `signal.reason`. Pass it to cancellable
70
+ * work (queries, `fetch`, timers) so a timed-out callback stops instead
71
+ * of running on. `run()` also stops waiting for the callback, rolls back
72
+ * and rejects with that error. A participant exposes the signal of the
73
+ * transaction it joined; a savepoint's signal also aborts with its
74
+ * parent's. Never aborts when no timeout is set.
75
+ */
76
+ readonly signal: AbortSignal;
67
77
  /** Commit the transaction. */
68
78
  commit(): Promise<void>;
69
- /** Rollback the transaction. */
79
+ /**
80
+ * Rollback the transaction. Idempotent once rolled back or failed; throws
81
+ * `TransactionStateError` on a committed transaction, which can no
82
+ * longer be undone.
83
+ */
70
84
  rollback(reason?: unknown): Promise<void>;
71
85
  /** Mark the transaction as rollback-only (prevents commit). */
72
86
  markRollbackOnly(reason?: unknown): void;
@@ -4,4 +4,5 @@
4
4
  * @module utils
5
5
  */
6
6
  export { isTerminalState, isModifiable, summarizeTransaction, } from "./utils.helper.js";
7
+ export { raceSignal } from "./utils.signal.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,5 @@
4
4
  * @module utils
5
5
  */
6
6
  export { isTerminalState, isModifiable, summarizeTransaction, } from "./utils.helper.js";
7
+ export { raceSignal } from "./utils.signal.js";
7
8
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Abort-signal helpers.
3
+ *
4
+ * @module utils/utils.signal
5
+ */
6
+ /**
7
+ * Resolves or rejects with `work`, or rejects with `signal.reason` as soon
8
+ * as `signal` aborts, whichever happens first.
9
+ *
10
+ * The abandoned `work` promise keeps running (JavaScript cannot cancel it);
11
+ * its eventual rejection is observed so it never surfaces as an unhandled
12
+ * rejection. Cooperative code should watch the same signal and stop.
13
+ */
14
+ export declare function raceSignal<T>(work: Promise<T>, signal: AbortSignal): Promise<T>;
15
+ //# sourceMappingURL=utils.signal.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Abort-signal helpers.
3
+ *
4
+ * @module utils/utils.signal
5
+ */
6
+ /**
7
+ * Resolves or rejects with `work`, or rejects with `signal.reason` as soon
8
+ * as `signal` aborts, whichever happens first.
9
+ *
10
+ * The abandoned `work` promise keeps running (JavaScript cannot cancel it);
11
+ * its eventual rejection is observed so it never surfaces as an unhandled
12
+ * rejection. Cooperative code should watch the same signal and stop.
13
+ */
14
+ export function raceSignal(work, signal) {
15
+ if (signal.aborted) {
16
+ work.catch(() => undefined);
17
+ return Promise.reject(signal.reason);
18
+ }
19
+ return new Promise((resolve, reject) => {
20
+ const onAbort = () => {
21
+ work.catch(() => undefined);
22
+ reject(signal.reason);
23
+ };
24
+ signal.addEventListener("abort", onAbort, { once: true });
25
+ work.then((value) => {
26
+ signal.removeEventListener("abort", onAbort);
27
+ resolve(value);
28
+ }, (error) => {
29
+ signal.removeEventListener("abort", onAbort);
30
+ reject(error);
31
+ });
32
+ });
33
+ }
34
+ //# sourceMappingURL=utils.signal.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/transactions",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Transaction lifecycle and coordination with state machine, AsyncLocalStorage context propagation, savepoints, hooks, and adapter abstraction.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -25,14 +25,14 @@
25
25
  "!dist/.tsbuildinfo"
26
26
  ],
27
27
  "dependencies": {
28
- "@zudojs/errors": "1.1.0"
28
+ "@zudojs/errors": "1.3.0"
29
29
  },
30
30
  "engines": {
31
31
  "node": ">=24.0.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "typescript": "7.0.2",
35
- "vitest": "^4.1.11"
35
+ "vitest": "^5.0.1"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
@@ -43,7 +43,7 @@
43
43
  "savepoints",
44
44
  "rollback"
45
45
  ],
46
- "homepage": "https://github.com/oyinlola-tech/zudo#readme",
46
+ "homepage": "https://zudojs.oyinlola.site/docs/packages-transactions",
47
47
  "bugs": {
48
48
  "url": "https://github.com/oyinlola-tech/zudo/issues"
49
49
  },