@tstdl/base 0.93.74 → 0.93.77

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.
@@ -1,25 +1,88 @@
1
+ /**
2
+ * Defines the visual formatting style for a list of instructions.
3
+ * - `sections`: Renders keys as Markdown headers (e.g., `# Header`).
4
+ * - `ordered`: Renders items as a numbered list (e.g., `1. Item`).
5
+ * - `unordered`: Renders items as a bulleted list (e.g., `- Item`).
6
+ */
1
7
  export type ListStyle = 'sections' | 'ordered' | 'unordered';
2
- type InstructionsListContent = string[] | Instructions;
8
+ /**
9
+ * The content of an instructions list, which can be either:
10
+ * - An array of strings (simple list items).
11
+ * - A nested `Instructions` object (key-value pairs or further lists).
12
+ */
13
+ export type InstructionsListContent = string[] | Instructions;
14
+ /**
15
+ * A container representing a specific grouping of instructions with a defined style.
16
+ * This is usually created via factory functions like `sections()`, `orderedList()`, or `unorderedList()`.
17
+ */
3
18
  export type InstructionsList = {
19
+ /** The rendering style to apply to this group. */
4
20
  style: ListStyle;
21
+ /** An optional high-level instruction or description associated with this group. */
5
22
  instruction?: string;
23
+ /** The content of the list, either an array of strings or a nested key-value map. */
6
24
  items: InstructionsListContent;
7
25
  };
26
+ /**
27
+ * A recursive dictionary structure for defining structured instructions.
28
+ * Keys typically represent labels or headers, while values represent the content.
29
+ */
8
30
  export type Instructions = {
9
31
  [key: string]: string | string[] | InstructionsList | Instructions;
10
32
  };
33
+ /**
34
+ * Creates a container where content is rendered as Markdown sections.
35
+ * Keys in the object map become headers (e.g., `# Key`), and nested items reset indentation.
36
+ *
37
+ * @param items - The content of the section.
38
+ */
11
39
  export declare function sections(items: InstructionsListContent): InstructionsList;
40
+ /**
41
+ * Creates a container where content is rendered as Markdown sections with a preamble.
42
+ *
43
+ * @param instruction - A general instruction describing this section.
44
+ * @param items - The content of the section.
45
+ */
12
46
  export declare function sections(instruction: string, items: InstructionsListContent): InstructionsList;
47
+ /**
48
+ * Creates a container rendered as a numbered list.
49
+ *
50
+ * @param items - The list items or map.
51
+ */
13
52
  export declare function orderedList(items: InstructionsListContent): InstructionsList;
53
+ /**
54
+ * Creates a container rendered as a numbered list with a preamble.
55
+ *
56
+ * @param instruction - An instruction describing the list.
57
+ * @param items - The list items or map.
58
+ */
14
59
  export declare function orderedList(instruction: string, items: InstructionsListContent): InstructionsList;
60
+ /**
61
+ * Creates a container rendered as a bulleted list.
62
+ *
63
+ * @param items - The list items or map.
64
+ */
15
65
  export declare function unorderedList(items: InstructionsListContent): InstructionsList;
66
+ /**
67
+ * Creates a container rendered as a bulleted list with a preamble.
68
+ *
69
+ * @param instruction - An instruction describing the list.
70
+ * @param items - The list items or map.
71
+ */
16
72
  export declare function unorderedList(instruction: string, items: InstructionsListContent): InstructionsList;
17
73
  /**
18
- * Formats instructions into a string representation suitable for AI prompts.
19
- * @param node
20
- * @param options
74
+ * Formats a structured instructions object into a string representation suitable for AI prompts (Markdown).
75
+ *
76
+ * It recursively handles:
77
+ * - `sections`: Creates headers (H1, H2...).
78
+ * - `ordered` / `unordered`: Creates indented lists.
79
+ * - `Instructions`: Objects are formatted as key-value pairs (e.g., `- **Key:** Value`).
80
+ *
81
+ * @param node - The root instructions object, array, or instructions list wrapper.
82
+ * @param options - Formatting options.
83
+ * @param options.initialDepth - The starting indentation level (default: 0).
84
+ * @returns A formatted string.
21
85
  */
22
86
  export declare function formatInstructions(node: Instructions | InstructionsList | string[], options?: {
23
87
  initialDepth?: number;
24
88
  }): string;
25
- export {};
@@ -146,9 +146,17 @@ function processNode(node, context) {
146
146
  }).join(separator);
147
147
  }
148
148
  /**
149
- * Formats instructions into a string representation suitable for AI prompts.
150
- * @param node
151
- * @param options
149
+ * Formats a structured instructions object into a string representation suitable for AI prompts (Markdown).
150
+ *
151
+ * It recursively handles:
152
+ * - `sections`: Creates headers (H1, H2...).
153
+ * - `ordered` / `unordered`: Creates indented lists.
154
+ * - `Instructions`: Objects are formatted as key-value pairs (e.g., `- **Key:** Value`).
155
+ *
156
+ * @param node - The root instructions object, array, or instructions list wrapper.
157
+ * @param options - Formatting options.
158
+ * @param options.initialDepth - The starting indentation level (default: 0).
159
+ * @returns A formatted string.
152
160
  */
153
161
  export function formatInstructions(node, options = {}) {
154
162
  // Heuristic: If passing a raw object, assume it's a Root Section unless specified otherwise.
@@ -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.77",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -150,7 +150,7 @@
150
150
  "@zxcvbn-ts/language-de": "^3.0",
151
151
  "@zxcvbn-ts/language-en": "^3.0",
152
152
  "drizzle-orm": "^0.45",
153
- "file-type": "^21.1",
153
+ "file-type": "^21.3",
154
154
  "genkit": "^1.27",
155
155
  "handlebars": "^4.7",
156
156
  "minio": "^8.0",
@@ -181,13 +181,13 @@
181
181
  "concurrently": "9.2",
182
182
  "drizzle-kit": "0.31",
183
183
  "eslint": "9.39",
184
- "globals": "16.5",
184
+ "globals": "17.0",
185
185
  "tsc-alias": "1.8",
186
186
  "typedoc-github-wiki-theme": "2.1",
187
187
  "typedoc-plugin-markdown": "4.9",
188
188
  "typedoc-plugin-missing-exports": "4.1",
189
189
  "typescript": "5.9",
190
- "typescript-eslint": "8.50"
190
+ "typescript-eslint": "8.51"
191
191
  },
192
192
  "overrides": {
193
193
  "drizzle-kit": {
@@ -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
  }