@tstdl/base 0.93.74 → 0.93.76

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.
@@ -34,7 +34,7 @@ let DocumentManagementObservationService = DocumentManagementObservationService_
34
34
  collectionChange(ids, transactionOrSession) {
35
35
  const transaction = tryGetTstdlTransaction(transactionOrSession);
36
36
  if (isDefined(transaction)) {
37
- transaction.afterCommit$.subscribe(() => this.collectionChange(ids));
37
+ transaction.afterCommit(() => this.collectionChange(ids));
38
38
  }
39
39
  else {
40
40
  for (const id of toArray(ids)) {
@@ -45,7 +45,7 @@ let DocumentManagementObservationService = DocumentManagementObservationService_
45
45
  documentChange(ids, transactionOrSession) {
46
46
  const transaction = tryGetTstdlTransaction(transactionOrSession);
47
47
  if (isDefined(transaction)) {
48
- transaction.afterCommit$.subscribe(() => this.documentChange(ids));
48
+ transaction.afterCommit(() => this.documentChange(ids));
49
49
  }
50
50
  else {
51
51
  for (const id of toArray(ids)) {
@@ -56,7 +56,7 @@ let DocumentManagementObservationService = DocumentManagementObservationService_
56
56
  workflowChange(ids, transactionOrSession) {
57
57
  const transaction = tryGetTstdlTransaction(transactionOrSession);
58
58
  if (isDefined(transaction)) {
59
- transaction.afterCommit$.subscribe(() => this.workflowChange(ids));
59
+ transaction.afterCommit(() => this.workflowChange(ids));
60
60
  }
61
61
  else {
62
62
  for (const id of toArray(ids)) {
@@ -67,7 +67,7 @@ let DocumentManagementObservationService = DocumentManagementObservationService_
67
67
  requestChange(ids, transactionOrSession) {
68
68
  const transaction = tryGetTstdlTransaction(transactionOrSession);
69
69
  if (isDefined(transaction)) {
70
- transaction.afterCommit$.subscribe(() => this.requestChange(ids));
70
+ transaction.afterCommit(() => this.requestChange(ids));
71
71
  }
72
72
  else {
73
73
  for (const id of toArray(ids)) {
@@ -1,5 +1,4 @@
1
1
  import { PgTransaction as DrizzlePgTransaction, type PgQueryResultHKT, type PgTransactionConfig } from 'drizzle-orm/pg-core';
2
- import { DeferredPromise } from '../../promise/deferred-promise.js';
3
2
  import type { Record } from '../../types/index.js';
4
3
  import type { Database } from './database.js';
5
4
  export type PgTransaction = DrizzlePgTransaction<PgQueryResultHKT, Record, Record>;
@@ -7,12 +6,13 @@ export { DrizzlePgTransaction };
7
6
  export type TransactionConfig = PgTransactionConfig;
8
7
  export declare abstract class Transaction implements AsyncDisposable {
9
8
  #private;
10
- readonly afterCommit$: import("rxjs").Observable<void>;
9
+ readonly afterCommit: import("../../utils/async-hook/index.js").AsyncHook<never, never, unknown>;
11
10
  manualCommit: boolean;
12
11
  [Symbol.asyncDispose](): Promise<void>;
13
12
  withManualCommit(): void;
14
13
  /**
15
- * Enters automatic transaction handling. Transaction will be commited when all use-calls are done or rolled back when one throws.
14
+ * Executes the handler within the transaction scope.
15
+ * Commits automatically on success if manual commit is disabled, or rolls back on error.
16
16
  */
17
17
  use<T>(handler: () => Promise<T>): Promise<T>;
18
18
  commit(): Promise<void>;
@@ -21,11 +21,11 @@ export declare abstract class Transaction implements AsyncDisposable {
21
21
  protected abstract _rollback(): void | Promise<void>;
22
22
  }
23
23
  export declare class DrizzleTransaction extends Transaction {
24
+ #private;
24
25
  readonly pgTransaction: PgTransaction;
25
- readonly deferPromise: DeferredPromise<void>;
26
- readonly pgTransactionPromise: Promise<void>;
27
- constructor(pgTransaction: PgTransaction, pgTransactionPromise: Promise<void>);
26
+ constructor(pgTransaction: PgTransaction);
28
27
  static create(session: Database | PgTransaction, config?: TransactionConfig): Promise<DrizzleTransaction>;
29
28
  protected _commit(): Promise<void>;
30
- protected _rollback(): void;
29
+ protected _rollback(): Promise<void>;
30
+ private setTransactionResultPromise;
31
31
  }
@@ -1,12 +1,11 @@
1
1
  import { PgTransaction as DrizzlePgTransaction } from 'drizzle-orm/pg-core';
2
- import { Subject } from 'rxjs';
3
2
  import { DeferredPromise } from '../../promise/deferred-promise.js';
3
+ import { asyncHook } from '../../utils/async-hook/index.js';
4
4
  export { DrizzlePgTransaction };
5
5
  export class Transaction {
6
- #afterCommitSubject = new Subject();
7
6
  #useCounter = 0;
8
7
  #done = false;
9
- afterCommit$ = this.#afterCommitSubject.asObservable();
8
+ afterCommit = asyncHook();
10
9
  manualCommit = false;
11
10
  async [Symbol.asyncDispose]() {
12
11
  if (!this.#done) {
@@ -17,65 +16,92 @@ export class Transaction {
17
16
  this.manualCommit = true;
18
17
  }
19
18
  /**
20
- * Enters automatic transaction handling. Transaction will be commited when all use-calls are done or rolled back when one throws.
19
+ * Executes the handler within the transaction scope.
20
+ * Commits automatically on success if manual commit is disabled, or rolls back on error.
21
21
  */
22
22
  async use(handler) {
23
+ if (this.#done) {
24
+ throw new Error('Transaction is already closed');
25
+ }
23
26
  this.#useCounter++;
24
27
  try {
25
28
  return await handler();
26
29
  }
30
+ catch (error) {
31
+ if (!this.#done) {
32
+ try {
33
+ await this.rollback();
34
+ }
35
+ catch { /* ignore */ }
36
+ }
37
+ throw error;
38
+ }
27
39
  finally {
28
40
  this.#useCounter--;
29
- if ((this.#useCounter == 0) && !this.#done && !this.manualCommit) {
41
+ if (!this.#done && !this.manualCommit && (this.#useCounter == 0)) {
30
42
  await this.commit();
31
43
  }
32
44
  }
33
45
  }
34
46
  async commit() {
47
+ if (this.#done) {
48
+ throw new Error('Transaction is already closed');
49
+ }
35
50
  this.#done = true;
36
51
  await this._commit();
37
- this.#afterCommitSubject.next();
38
- this.#afterCommitSubject.complete();
52
+ await this.afterCommit.trigger();
39
53
  }
40
54
  async rollback() {
55
+ if (this.#done) {
56
+ return;
57
+ }
41
58
  this.#done = true;
42
- this.#afterCommitSubject.complete();
43
59
  await this._rollback();
44
60
  }
45
61
  }
46
62
  export class DrizzleTransaction extends Transaction {
63
+ #deferPromise = new DeferredPromise();
47
64
  pgTransaction;
48
- deferPromise = new DeferredPromise();
49
- pgTransactionPromise;
50
- constructor(pgTransaction, pgTransactionPromise) {
65
+ #pgTransactionResultPromise;
66
+ constructor(pgTransaction) {
51
67
  super();
52
68
  this.pgTransaction = pgTransaction;
53
- this.pgTransactionPromise = pgTransactionPromise;
54
69
  }
55
70
  static async create(session, config) {
56
- const transactionPromise = new DeferredPromise();
57
- const pgTransactionPromise = session.transaction(async (tx) => {
58
- const transaction = new DrizzleTransaction(tx, pgTransactionPromise);
59
- transactionPromise.resolve(transaction);
60
- await transaction.deferPromise;
71
+ const instancePromise = new DeferredPromise();
72
+ const pgTransactionResultPromise = session.transaction(async (tx) => {
73
+ const transaction = new DrizzleTransaction(tx);
74
+ instancePromise.resolve(transaction);
75
+ await transaction.#deferPromise;
61
76
  }, config);
62
- pgTransactionPromise.catch((error) => {
63
- if (transactionPromise.pending) {
64
- transactionPromise.reject(error);
77
+ pgTransactionResultPromise.catch((error) => {
78
+ if (instancePromise.pending) {
79
+ instancePromise.reject(error);
65
80
  }
66
81
  });
67
- return await transactionPromise;
82
+ const transaction = await instancePromise;
83
+ transaction.setTransactionResultPromise(pgTransactionResultPromise);
84
+ return transaction;
68
85
  }
69
86
  async _commit() {
70
- this.deferPromise.resolve();
71
- await this.pgTransactionPromise;
87
+ this.#deferPromise.resolve();
88
+ await this.#pgTransactionResultPromise;
72
89
  }
73
- _rollback() {
90
+ async _rollback() {
74
91
  try {
75
92
  this.pgTransaction.rollback();
76
93
  }
77
94
  catch (error) {
78
- this.deferPromise.reject(error);
95
+ if (this.#deferPromise.pending) {
96
+ this.#deferPromise.reject(error);
97
+ }
79
98
  }
99
+ try {
100
+ await this.#pgTransactionResultPromise;
101
+ }
102
+ catch { /* expect rejection during rollback */ }
103
+ }
104
+ setTransactionResultPromise(promise) {
105
+ this.#pgTransactionResultPromise = promise;
80
106
  }
81
107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.74",
3
+ "version": "0.93.76",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,3 +1,26 @@
1
+ /**
2
+ * Defines the signature for a handler function that can be registered with an `AsyncHook`.
3
+ * The function can be synchronous (returning `R`) or asynchronous (returning `Promise<R>`).
4
+ *
5
+ * The signature is conditional based on the value type `T` and context type `C`.
6
+ *
7
+ * @template T The type of the value passed to the handler.
8
+ * @template C The type of the optional context object.
9
+ * @template R The expected return type of the handler.
10
+ */
11
+ export type AsyncHookHandler<T, C, R> = [
12
+ C
13
+ ] extends [never] ? [T] extends [never] ? () => R | Promise<R> : (value: T) => R | Promise<R> : [T] extends [never] ? (value: undefined, context: C) => R | Promise<R> : (value: T, context: C) => R | Promise<R>;
14
+ /**
15
+ * Defines the signature for the trigger method.
16
+ *
17
+ * @template T The type of the value passed to the trigger.
18
+ * @template C The type of the optional context object.
19
+ * @template R The return type of an individual handler.
20
+ */
21
+ export type AsyncHookTrigger<T, C, R> = [
22
+ C
23
+ ] extends [never] ? [T] extends [never] ? () => Promise<R[]> : (value: T) => Promise<R[]> : [T] extends [never] ? (value: undefined, context: C) => Promise<R[]> : (value: T, context: C) => Promise<R[]>;
1
24
  /**
2
25
  * Represents the public interface for an asynchronous hook.
3
26
  *
@@ -6,38 +29,35 @@
6
29
  * @template R The return type of an individual handler. Defaults to `unknown`.
7
30
  */
8
31
  export type AsyncHook<T, C = never, R = unknown> = {
32
+ /**
33
+ * Registers a handler function via the callable interface.
34
+ * @param handler The async handler function to register.
35
+ */
36
+ (handler: AsyncHookHandler<T, C, R>): AsyncHookHandlerRegistration;
9
37
  /**
10
38
  * Registers a handler function to be called when the hook is triggered.
11
39
  * @param handler The async handler function to register.
12
- * @returns A registration object with an `unregister` method to remove the handler.
13
40
  */
14
41
  register: (handler: AsyncHookHandler<T, C, R>) => AsyncHookHandlerRegistration;
15
42
  /**
16
43
  * Triggers the hook, executing all registered handlers in sequence.
44
+ * If any handler throws an error, execution stops and the promise rejects.
17
45
  *
18
46
  * The signature of this function is conditional:
19
- * - If the context type `C` is `never` (the default), it accepts only the `value` argument.
20
- * - If `C` is any other type, it requires both `value` and `context` arguments.
47
+ * - `T=never, C=never`: `trigger()`
48
+ * - `T=Type, C=never`: `trigger(value)`
49
+ * - `T=never, C=Type`: `trigger(undefined, context)`
50
+ * - `T=Type, C=Type`: `trigger(value, context)`
21
51
  *
22
- * @param value The value to pass to all handlers.
23
- * @param context The context object to pass to all handlers (only required if `C` is not `never`).
24
52
  * @returns A promise that resolves to an array of results from all handlers.
25
53
  */
26
- trigger: [C] extends [never] ? ((value: T) => Promise<R[]>) : ((value: T, context: C) => Promise<R[]>);
54
+ trigger: AsyncHookTrigger<T, C, R>;
55
+ /**
56
+ * Removes all registered handlers.
57
+ * Useful for cleanup logic or resetting state in tests.
58
+ */
59
+ removeAll: () => void;
27
60
  };
28
- /**
29
- * Defines the signature for a handler function that can be registered with an `AsyncHook`.
30
- * The function can be synchronous (returning `R`) or asynchronous (returning `Promise<R>`).
31
- *
32
- * The signature is conditional based on the context type `C`:
33
- * - If `C` is `never`, the handler receives only the `value` argument.
34
- * - If `C` is any other type, the handler receives both `value` and `context`.
35
- *
36
- * @template T The type of the value passed to the handler.
37
- * @template C The type of the optional context object.
38
- * @template R The expected return type of the handler.
39
- */
40
- export type AsyncHookHandler<T, C, R> = [C] extends [never] ? ((value: T) => R | Promise<R>) : ((value: T, context: C) => R | Promise<R>);
41
61
  /**
42
62
  * Represents the object returned when a handler is registered,
43
63
  * allowing for its subsequent unregistration.
@@ -52,67 +72,40 @@ export type AsyncHookHandlerRegistration = {
52
72
  * Creates a new asynchronous hook.
53
73
  *
54
74
  * An async hook is a system that allows you to register multiple "handler" functions
55
- * that will be executed in sequence when a "trigger" event occurs. This is useful
56
- * for creating extensible, plugin-like architectures. Handlers can be synchronous
57
- * or asynchronous.
75
+ * that will be executed in **sequential order** when a "trigger" event occurs.
76
+ * This is useful for creating extensible, plugin-like architectures.
77
+ * Handlers can be synchronous or asynchronous.
58
78
  *
59
- * @template T The type of the primary value that the hook is triggered with.
79
+ * @template T The type of the primary value that the hook is triggered with. Defaults to `never` (no value).
60
80
  * @template C The type of the optional context object passed to the hook's trigger and handlers. Defaults to `never`.
61
81
  * @template R The return type of an individual handler. The `trigger` method will resolve with an array of these values (`R[]`). Defaults to `unknown`.
62
- * @returns {AsyncHook<T, C, R>} An object with `register` and `trigger` methods.
63
82
  *
64
83
  * @example
65
84
  * ```ts
66
- * // Simple hook without context
67
- * async function runSimpleExample() {
68
- * const onTaskStart = asyncHook<string>();
69
- *
70
- * onTaskStart.register(taskName => {
71
- * console.log(`[Logger] Task started: ${taskName}`);
72
- * });
73
- *
74
- * const registration = onTaskStart.register(async taskName => {
75
- * await new Promise(resolve => setTimeout(resolve, 50));
76
- * console.log(`[Notifier] Notifying that task started: ${taskName}`);
77
- * });
78
- *
79
- * await onTaskStart.trigger('Process Data');
80
- * // [Logger] Task started: Process Data
81
- * // [Notifier] Notifying that task started: Process Data
82
- *
83
- * registration.unregister();
84
- * console.log('Notifier unregistered.');
85
- *
86
- * await onTaskStart.trigger('Finalize Report');
87
- * // [Logger] Task started: Finalize Report
88
- * }
85
+ * // 1. Simple hook (Signal only, no data)
86
+ * const onInit = asyncHook();
87
+ * onInit(() => console.log('Initialized'));
88
+ * await onInit.trigger();
89
89
  * ```
90
90
  *
91
91
  * @example
92
92
  * ```ts
93
- * // Hook with a context object
94
- * async function runContextExample() {
95
- * type TaskContext = { userId: number; transactionId: string };
96
- *
97
- * const onTaskComplete = asyncHook<string, TaskContext, boolean>();
98
- *
99
- * onTaskComplete.register((taskName, context) => {
100
- * console.log(`[Audit] Task '${taskName}' completed by user ${context.userId}.`);
101
- * return true; // Audit successful
102
- * });
93
+ * // 2. Hook with data, no context
94
+ * const onMessage = asyncHook<string>();
95
+ * onMessage((msg) => console.log('Received:', msg));
96
+ * await onMessage.trigger('Hello');
97
+ * ```
103
98
  *
104
- * onTaskComplete.register(async (taskName, context) => {
105
- * console.log(`[DB] Logging completion of '${taskName}' for transaction ${context.transactionId}.`);
106
- * return true; // DB update successful
107
- * });
99
+ * @example
100
+ * ```ts
101
+ * // 3. Hook with context
102
+ * type Context = { user: string };
103
+ * const onAction = asyncHook<string, Context>();
108
104
  *
109
- * const results = await onTaskComplete.trigger(
110
- * 'SubmitOrder',
111
- * { userId: 123, transactionId: 'abc-456' }
112
- * );
105
+ * // Note: Handlers receive context as the second argument
106
+ * onAction((action, ctx) => console.log(action, ctx.user));
113
107
  *
114
- * console.log('Handler results:', results); // [true, true]
115
- * }
108
+ * await onAction.trigger('Click', { user: 'Alice' });
116
109
  * ```
117
110
  */
118
- export declare function asyncHook<T, C = never, R = unknown>(): AsyncHook<T, C, R>;
111
+ export declare function asyncHook<T = never, C = never, R = unknown>(): AsyncHook<T, C, R>;
@@ -2,94 +2,71 @@
2
2
  * Creates a new asynchronous hook.
3
3
  *
4
4
  * An async hook is a system that allows you to register multiple "handler" functions
5
- * that will be executed in sequence when a "trigger" event occurs. This is useful
6
- * for creating extensible, plugin-like architectures. Handlers can be synchronous
7
- * or asynchronous.
5
+ * that will be executed in **sequential order** when a "trigger" event occurs.
6
+ * This is useful for creating extensible, plugin-like architectures.
7
+ * Handlers can be synchronous or asynchronous.
8
8
  *
9
- * @template T The type of the primary value that the hook is triggered with.
9
+ * @template T The type of the primary value that the hook is triggered with. Defaults to `never` (no value).
10
10
  * @template C The type of the optional context object passed to the hook's trigger and handlers. Defaults to `never`.
11
11
  * @template R The return type of an individual handler. The `trigger` method will resolve with an array of these values (`R[]`). Defaults to `unknown`.
12
- * @returns {AsyncHook<T, C, R>} An object with `register` and `trigger` methods.
13
12
  *
14
13
  * @example
15
14
  * ```ts
16
- * // Simple hook without context
17
- * async function runSimpleExample() {
18
- * const onTaskStart = asyncHook<string>();
19
- *
20
- * onTaskStart.register(taskName => {
21
- * console.log(`[Logger] Task started: ${taskName}`);
22
- * });
23
- *
24
- * const registration = onTaskStart.register(async taskName => {
25
- * await new Promise(resolve => setTimeout(resolve, 50));
26
- * console.log(`[Notifier] Notifying that task started: ${taskName}`);
27
- * });
28
- *
29
- * await onTaskStart.trigger('Process Data');
30
- * // [Logger] Task started: Process Data
31
- * // [Notifier] Notifying that task started: Process Data
32
- *
33
- * registration.unregister();
34
- * console.log('Notifier unregistered.');
35
- *
36
- * await onTaskStart.trigger('Finalize Report');
37
- * // [Logger] Task started: Finalize Report
38
- * }
15
+ * // 1. Simple hook (Signal only, no data)
16
+ * const onInit = asyncHook();
17
+ * onInit(() => console.log('Initialized'));
18
+ * await onInit.trigger();
39
19
  * ```
40
20
  *
41
21
  * @example
42
22
  * ```ts
43
- * // Hook with a context object
44
- * async function runContextExample() {
45
- * type TaskContext = { userId: number; transactionId: string };
46
- *
47
- * const onTaskComplete = asyncHook<string, TaskContext, boolean>();
48
- *
49
- * onTaskComplete.register((taskName, context) => {
50
- * console.log(`[Audit] Task '${taskName}' completed by user ${context.userId}.`);
51
- * return true; // Audit successful
52
- * });
23
+ * // 2. Hook with data, no context
24
+ * const onMessage = asyncHook<string>();
25
+ * onMessage((msg) => console.log('Received:', msg));
26
+ * await onMessage.trigger('Hello');
27
+ * ```
53
28
  *
54
- * onTaskComplete.register(async (taskName, context) => {
55
- * console.log(`[DB] Logging completion of '${taskName}' for transaction ${context.transactionId}.`);
56
- * return true; // DB update successful
57
- * });
29
+ * @example
30
+ * ```ts
31
+ * // 3. Hook with context
32
+ * type Context = { user: string };
33
+ * const onAction = asyncHook<string, Context>();
58
34
  *
59
- * const results = await onTaskComplete.trigger(
60
- * 'SubmitOrder',
61
- * { userId: 123, transactionId: 'abc-456' }
62
- * );
35
+ * // Note: Handlers receive context as the second argument
36
+ * onAction((action, ctx) => console.log(action, ctx.user));
63
37
  *
64
- * console.log('Handler results:', results); // [true, true]
65
- * }
38
+ * await onAction.trigger('Click', { user: 'Alice' });
66
39
  * ```
67
40
  */
68
41
  export function asyncHook() {
69
- const handlers = [];
70
- return {
71
- register: (handler) => {
72
- handlers.push(handler);
73
- return {
42
+ const handles = new Set();
43
+ function register(handler) {
44
+ const handle = {
45
+ handler,
46
+ registration: {
74
47
  unregister() {
75
- const index = handlers.indexOf(handler);
76
- if (index > -1) {
77
- handlers.splice(index, 1);
78
- }
48
+ handles.delete(handle);
79
49
  },
80
- };
81
- },
82
- // The implementation uses a single function body, but the public type signature
83
- // is conditional, ensuring type safety for callers.
84
- trigger: async (value, context) => {
85
- const returnValues = [];
86
- // Create a snapshot of handlers in case one of them unregisters another during its execution.
87
- for (const handler of [...handlers]) {
88
- // The non-null assertion `context!` is safe due to the conditional public type of `trigger`.
89
- const returnValue = await handler(value, context);
90
- returnValues.push(returnValue);
91
- }
92
- return returnValues;
93
- },
94
- };
50
+ },
51
+ };
52
+ handles.add(handle);
53
+ return handle.registration;
54
+ }
55
+ async function trigger(value, context) {
56
+ const returnValues = [];
57
+ // Create a snapshot of handlers to safely handle unregistrations during execution.
58
+ for (const handle of [...handles]) {
59
+ const returnValue = await handle.handler(value, context);
60
+ returnValues.push(returnValue);
61
+ }
62
+ return returnValues;
63
+ }
64
+ function removeAll() {
65
+ handles.clear();
66
+ }
67
+ const hook = register;
68
+ hook.register = register;
69
+ hook.trigger = trigger;
70
+ hook.removeAll = removeAll;
71
+ return hook;
95
72
  }