@logosdx/hooks 1.0.0-beta.2 → 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.
@@ -1,18 +1,16 @@
1
- import { AsyncFunc, FunctionProps } from '@logosdx/utils';
1
+ import { FunctionProps } from '@logosdx/utils';
2
2
  /**
3
3
  * Error thrown when a hook calls `ctx.fail()`.
4
4
  *
5
- * This error is only created when using the default `handleFail` behavior.
5
+ * Only created when using the default `handleFail` behavior.
6
6
  * If a custom `handleFail` is provided, that error type is thrown instead.
7
7
  *
8
8
  * @example
9
- * hooks.on('validate', async (ctx) => {
10
- * if (!ctx.args[0].isValid) {
11
- * ctx.fail('Validation failed');
12
- * }
9
+ * hooks.add('validate', (data, ctx) => {
10
+ * if (!data.isValid) ctx.fail('Validation failed');
13
11
  * });
14
12
  *
15
- * const [, err] = await attempt(() => engine.emit('validate', data));
13
+ * const [, err] = await attempt(() => engine.run('validate', data));
16
14
  * if (isHookError(err)) {
17
15
  * console.log(err.hookName); // 'validate'
18
16
  * }
@@ -28,137 +26,255 @@ export declare class HookError extends Error {
28
26
  * Type guard to check if an error is a HookError.
29
27
  *
30
28
  * @example
31
- * const { error } = await engine.emit('validate', data);
32
- * if (isHookError(error)) {
33
- * console.log(`Hook "${error.hookName}" failed`);
29
+ * const [, err] = await attempt(() => engine.run('validate', data));
30
+ * if (isHookError(err)) {
31
+ * console.log(`Hook "${err.hookName}" failed`);
34
32
  * }
35
33
  */
36
34
  export declare const isHookError: (error: unknown) => error is HookError;
37
35
  /**
38
- * Result returned from `emit()` after running all hook callbacks.
36
+ * Custom error handler for `ctx.fail()`.
37
+ * Can be an Error constructor or a function that throws.
39
38
  */
40
- export interface EmitResult<F extends AsyncFunc> {
41
- /** Current arguments (possibly modified by callbacks) */
42
- args: Parameters<F>;
43
- /** Result value (if set by a callback) */
44
- result?: Awaited<ReturnType<F>> | undefined;
45
- /** Whether a callback called `returnEarly()` */
46
- earlyReturn: boolean;
47
- }
39
+ export type HandleFail<Args extends unknown[] = [string]> = (new (...args: Args) => Error) | ((...args: Args) => never);
48
40
  /**
49
- * Context object passed to hook callbacks.
50
- * Provides access to arguments, results, and control methods.
41
+ * Request-scoped state bag that flows across hook runs and engine instances.
42
+ *
43
+ * Use symbols for private plugin state and strings for shared cross-plugin contracts.
51
44
  *
52
45
  * @example
53
- * hooks.on('cacheCheck', async (ctx) => {
54
- * const [url] = ctx.args;
55
- * const cached = cache.get(url);
46
+ * // Private plugin state (symbol key)
47
+ * const CACHE_STATE = Symbol('cache');
48
+ * ctx.scope.set(CACHE_STATE, { key, rule });
56
49
  *
57
- * if (cached) {
58
- * ctx.setResult(cached);
59
- * ctx.returnEarly();
60
- * }
61
- * });
50
+ * // Shared cross-plugin contract (string key)
51
+ * ctx.scope.set('serializedKey', key);
62
52
  */
63
- export interface HookContext<F extends AsyncFunc, FailArgs extends unknown[] = [string]> {
64
- /** Current arguments passed to emit() */
65
- args: Parameters<F>;
66
- /** Result value (can be set by callbacks) */
67
- result?: Awaited<ReturnType<F>>;
68
- /** Abort hook execution with an error. */
69
- fail: (...args: FailArgs) => never;
70
- /** Replace the arguments for subsequent callbacks */
71
- setArgs: (next: Parameters<F>) => void;
72
- /** Set the result value */
73
- setResult: (next: Awaited<ReturnType<F>>) => void;
74
- /** Stop processing remaining callbacks and return early */
75
- returnEarly: () => void;
76
- /** Remove this callback from the hook */
77
- removeHook: () => void;
53
+ export declare class HookScope {
54
+ /**
55
+ * Get a value from the scope.
56
+ *
57
+ * @example
58
+ * const state = ctx.scope.get<CacheState>(CACHE_STATE);
59
+ */
60
+ get<T = unknown>(key: symbol | string): T | undefined;
61
+ /**
62
+ * Set a value in the scope.
63
+ *
64
+ * @example
65
+ * ctx.scope.set(CACHE_STATE, { key: 'abc', rule });
66
+ */
67
+ set<T = unknown>(key: symbol | string, value: T): void;
68
+ /**
69
+ * Check if a key exists in the scope.
70
+ */
71
+ has(key: symbol | string): boolean;
72
+ /**
73
+ * Delete a key from the scope.
74
+ */
75
+ delete(key: symbol | string): boolean;
78
76
  }
79
- export type HookFn<F extends AsyncFunc, FailArgs extends unknown[] = [string]> = (ctx: HookContext<F, FailArgs>) => Promise<void>;
80
- type HookOptions<F extends AsyncFunc, FailArgs extends unknown[] = [string]> = {
81
- callback: HookFn<F, FailArgs>;
82
- once?: true;
83
- ignoreOnFail?: true;
84
- };
85
- type HookOrOptions<F extends AsyncFunc, FailArgs extends unknown[] = [string]> = HookFn<F, FailArgs> | HookOptions<F, FailArgs>;
86
- type FuncOrNever<T> = T extends AsyncFunc ? T : never;
77
+ declare const EARLY_RETURN: unique symbol;
78
+ type EarlyReturnSignal = typeof EARLY_RETURN;
79
+ type FuncOrNever<T> = T extends (...args: any[]) => any ? T : never;
87
80
  /**
88
- * Custom error handler for `ctx.fail()`.
89
- * Can be an Error constructor or a function that throws.
81
+ * Extract only function property keys from a type.
82
+ *
83
+ * @example
84
+ * interface Doc {
85
+ * id: string;
86
+ * save(): Promise<void>;
87
+ * delete(): Promise<void>;
88
+ * }
89
+ *
90
+ * type DocHooks = HookName<Doc>; // 'save' | 'delete' (excludes 'id')
90
91
  */
91
- export type HandleFail<Args extends unknown[] = [string]> = (new (...args: Args) => Error) | ((...args: Args) => never);
92
+ export type HookName<T> = FunctionProps<T>;
92
93
  /**
93
- * Options for HookEngine constructor.
94
+ * Context object passed as the last argument to hook callbacks.
95
+ * Provides methods to modify args, short-circuit, fail, or self-remove.
96
+ *
97
+ * @example
98
+ * // Replace args for downstream callbacks
99
+ * hooks.add('beforeRequest', (url, opts, ctx) => {
100
+ * ctx.args(url, { ...opts, cache: 'no-store' });
101
+ * });
102
+ *
103
+ * // Short-circuit: replace args AND stop the chain
104
+ * hooks.add('beforeRequest', (url, opts, ctx) => {
105
+ * return ctx.args(normalizedUrl, opts);
106
+ * });
107
+ *
108
+ * // Short-circuit with a result value
109
+ * hooks.add('beforeRequest', (url, opts, ctx) => {
110
+ * const cached = cache.get(url);
111
+ * if (cached) return ctx.returns(cached);
112
+ * });
94
113
  */
95
- export interface HookEngineOptions<FailArgs extends unknown[] = [string]> {
114
+ export declare class HookContext<Args extends unknown[] = unknown[], Result = unknown, FailArgs extends unknown[] = [string]> {
96
115
  /**
97
- * Custom handler for `ctx.fail()`.
98
- * Can be an Error constructor or a function that throws.
116
+ * Request-scoped state bag shared across hook runs and engine instances.
99
117
  *
100
118
  * @example
101
- * // Use Firebase HttpsError
102
- * new HookEngine({ handleFail: HttpsError });
119
+ * // In beforeRequest hook
120
+ * ctx.scope.set(CACHE_KEY, serializedKey);
121
+ *
122
+ * // In afterRequest hook (same scope)
123
+ * const key = ctx.scope.get<string>(CACHE_KEY);
124
+ */
125
+ readonly scope: HookScope;
126
+ /** @internal */
127
+ constructor(handleFail: HandleFail<FailArgs>, hookName: string, removeFn: () => void, scope: HookScope);
128
+ /**
129
+ * Replace args for downstream callbacks.
130
+ * When used with `return`, also stops the chain.
131
+ *
132
+ * @example
133
+ * // Just replace args, continue chain
134
+ * ctx.args(newUrl, newOpts);
135
+ *
136
+ * // Replace args AND stop the chain
137
+ * return ctx.args(newUrl, newOpts);
138
+ */
139
+ args(...args: Args): EarlyReturnSignal;
140
+ /**
141
+ * Set a result value and stop the chain.
142
+ * Always used with `return`.
103
143
  *
104
- * // Use custom function
105
- * new HookEngine({
106
- * handleFail: (msg, data) => { throw Boom.badRequest(msg, data); }
144
+ * @example
145
+ * return ctx.returns(cachedResponse);
146
+ */
147
+ returns(value: Result): EarlyReturnSignal;
148
+ /**
149
+ * Abort hook execution with an error.
150
+ * Uses the engine's `handleFail` to create the error.
151
+ *
152
+ * @example
153
+ * ctx.fail('Validation failed');
154
+ */
155
+ fail(...args: FailArgs): never;
156
+ /**
157
+ * Remove this callback from future runs.
158
+ *
159
+ * @example
160
+ * hooks.add('init', (config, ctx) => {
161
+ * bootstrap(config);
162
+ * ctx.removeHook();
107
163
  * });
108
164
  */
109
- handleFail?: HandleFail<FailArgs>;
165
+ removeHook(): void;
166
+ /** @internal */
167
+ get _argsChanged(): boolean;
168
+ /** @internal */
169
+ get _newArgs(): Args | undefined;
170
+ /** @internal */
171
+ get _result(): Result | undefined;
172
+ /** @internal */
173
+ get _earlyReturn(): boolean;
110
174
  }
175
+ /**
176
+ * Context object passed as the last argument to pipe middleware callbacks.
177
+ * Simpler than HookContext — no `returns()` needed since you control
178
+ * flow by calling or not calling `next()`.
179
+ *
180
+ * @example
181
+ * hooks.add('execute', async (next, opts, ctx) => {
182
+ * // Modify opts for inner layers
183
+ * ctx.args({ ...opts, headers: { ...opts.headers, Auth: token } });
184
+ *
185
+ * // Call next to continue the chain, or don't to short-circuit
186
+ * return next();
187
+ * });
188
+ */
189
+ export declare class PipeContext<FailArgs extends unknown[] = [string]> {
190
+ /**
191
+ * Request-scoped state bag shared across hook runs and engine instances.
192
+ */
193
+ readonly scope: HookScope;
194
+ /** @internal */
195
+ constructor(handleFail: HandleFail<FailArgs>, hookName: string, removeFn: () => void, scope: HookScope, setArgs: (args: unknown[]) => void);
196
+ /**
197
+ * Replace args for `next()` and downstream middleware.
198
+ *
199
+ * @example
200
+ * ctx.args({ ...opts, timeout: 5000 });
201
+ * return next(); // next receives modified opts
202
+ */
203
+ args(...args: unknown[]): void;
204
+ /**
205
+ * Abort execution with an error.
206
+ *
207
+ * @example
208
+ * ctx.fail('Rate limit exceeded');
209
+ */
210
+ fail(...args: FailArgs): never;
211
+ /**
212
+ * Remove this middleware from future runs.
213
+ */
214
+ removeHook(): void;
215
+ }
216
+ /**
217
+ * Callback type for hooks. Receives spread args + ctx as last param.
218
+ *
219
+ * @example
220
+ * type BeforeRequest = HookCallback<(url: string, opts: RequestInit) => Promise<Response>>;
221
+ * // (url: string, opts: RequestInit, ctx: HookContext) => void | EarlyReturnSignal | Promise<...>
222
+ */
223
+ export type HookCallback<F extends (...args: any[]) => any = (...args: any[]) => any, FailArgs extends unknown[] = [string]> = F extends (...args: infer A) => infer R ? (...args: [...A, HookContext<A, Awaited<R>, FailArgs>]) => void | EarlyReturnSignal | Promise<void | EarlyReturnSignal> : never;
224
+ /**
225
+ * Callback type for pipe middleware. Receives `(next, ...args, ctx)`.
226
+ *
227
+ * @example
228
+ * type ExecuteMiddleware = PipeCallback<[opts: RequestOpts]>;
229
+ * // (next: () => Promise<R>, opts: RequestOpts, ctx: PipeContext) => R | Promise<R>
230
+ */
231
+ export type PipeCallback<Args extends unknown[] = unknown[], R = unknown, FailArgs extends unknown[] = [string]> = (next: () => R | Promise<R>, ...args: [...Args, PipeContext<FailArgs>]) => R | Promise<R>;
232
+ /**
233
+ * Result returned from `run()`/`runSync()` after executing all hook callbacks.
234
+ */
235
+ export interface RunResult<F extends (...args: any[]) => any = (...args: any[]) => any> {
236
+ /** Current arguments (possibly modified by callbacks) */
237
+ args: Parameters<F>;
238
+ /** Result value (if set via `ctx.returns()`) */
239
+ result: Awaited<ReturnType<F>> | undefined;
240
+ /** Whether a callback short-circuited via `return ctx.returns()` or `return ctx.args()` */
241
+ returned: boolean;
242
+ /** The scope used during this run (pass to subsequent runs to share state) */
243
+ scope: HookScope;
244
+ }
245
+ type DefaultLifecycle = Record<string, (...args: any[]) => any>;
111
246
  /**
112
247
  * A lightweight, type-safe lifecycle hook system.
113
248
  *
114
249
  * HookEngine allows you to define lifecycle events and subscribe to them.
115
- * Callbacks can modify arguments, set results, or abort execution.
250
+ * Callbacks receive spread arguments with a context object as the last param.
116
251
  *
117
252
  * @example
118
253
  * interface FetchLifecycle {
119
- * preRequest(url: string, options: RequestInit): Promise<Response>;
120
- * rateLimit(error: Error, attempt: number): Promise<void>;
121
- * cacheHit(url: string, data: unknown): Promise<unknown>;
254
+ * beforeRequest(url: string, options: RequestInit): Promise<Response>;
255
+ * afterRequest(response: Response, url: string): Promise<Response>;
122
256
  * }
123
257
  *
124
258
  * const hooks = new HookEngine<FetchLifecycle>();
125
259
  *
126
- * hooks.on('rateLimit', async (ctx) => {
127
- * const [error, attempt] = ctx.args;
128
- * if (attempt > 3) ctx.fail('Max retries exceeded');
129
- * await sleep(error.retryAfter * 1000);
260
+ * hooks.add('beforeRequest', (url, opts, ctx) => {
261
+ * ctx.args(url, { ...opts, headers: { ...opts.headers, 'X-Token': token } });
130
262
  * });
131
263
  *
132
- * hooks.on('cacheHit', async (ctx) => {
133
- * console.log('Cache hit for:', ctx.args[0]);
264
+ * hooks.add('beforeRequest', (url, opts, ctx) => {
265
+ * const cached = cache.get(url);
266
+ * if (cached) return ctx.returns(cached);
134
267
  * });
135
268
  *
136
- * // In your implementation
137
- * const result = await hooks.emit('cacheHit', url, cachedData);
269
+ * const pre = await hooks.run('beforeRequest', url, options);
270
+ * if (pre.returned) return pre.result;
271
+ * const response = await fetch(...pre.args);
138
272
  *
139
273
  * @typeParam Lifecycle - Interface defining the lifecycle hooks
140
274
  * @typeParam FailArgs - Arguments type for ctx.fail() (default: [string])
141
275
  */
142
- /**
143
- * Default permissive lifecycle type when no type parameter is provided.
144
- */
145
- type DefaultLifecycle = Record<string, AsyncFunc>;
146
- /**
147
- * Extract only function property keys from a type.
148
- * This ensures only methods are available as hook names, not data properties.
149
- *
150
- * @example
151
- * interface Doc {
152
- * id: string;
153
- * save(): Promise<void>;
154
- * delete(): Promise<void>;
155
- * }
156
- *
157
- * type DocHooks = HookName<Doc>; // 'save' | 'delete' (excludes 'id')
158
- */
159
- export type HookName<T> = FunctionProps<T>;
160
276
  export declare class HookEngine<Lifecycle = DefaultLifecycle, FailArgs extends unknown[] = [string]> {
161
- constructor(options?: HookEngineOptions<FailArgs>);
277
+ constructor(options?: HookEngine.Options<FailArgs>);
162
278
  /**
163
279
  * Register hook names for runtime validation.
164
280
  * Once any hooks are registered, all hooks must be registered before use.
@@ -168,125 +284,200 @@ export declare class HookEngine<Lifecycle = DefaultLifecycle, FailArgs extends u
168
284
  *
169
285
  * @example
170
286
  * const hooks = new HookEngine<FetchLifecycle>()
171
- * .register('preRequest', 'postRequest', 'rateLimit');
287
+ * .register('beforeRequest', 'afterRequest');
172
288
  *
173
- * hooks.on('preRequest', cb); // OK
174
- * hooks.on('preRequset', cb); // Error: not registered (typo caught!)
289
+ * hooks.add('beforeRequest', cb); // OK
290
+ * hooks.add('beforeRequset', cb); // Error: not registered (typo caught!)
175
291
  */
176
292
  register(...names: HookName<Lifecycle>[]): this;
177
293
  /**
178
294
  * Subscribe to a lifecycle hook.
179
295
  *
180
296
  * @param name - Name of the lifecycle hook
181
- * @param cbOrOpts - Callback function or options object
297
+ * @param callback - Callback function receiving spread args + ctx
298
+ * @param options - Options for this subscription
182
299
  * @returns Cleanup function to remove the subscription
183
300
  *
184
301
  * @example
185
302
  * // Simple callback
186
- * const cleanup = hooks.on('preRequest', async (ctx) => {
187
- * console.log('Request:', ctx.args[0]);
303
+ * const cleanup = hooks.add('beforeRequest', (url, opts, ctx) => {
304
+ * console.log('Request:', url);
188
305
  * });
189
306
  *
190
307
  * // With options
191
- * hooks.on('analytics', {
192
- * callback: async (ctx) => { track(ctx.args); },
193
- * once: true, // Remove after first run
194
- * ignoreOnFail: true // Don't throw if callback fails
195
- * });
308
+ * hooks.add('analytics', (event, ctx) => {
309
+ * track(event);
310
+ * }, { once: true, ignoreOnFail: true });
311
+ *
312
+ * // With priority (lower runs first)
313
+ * hooks.add('beforeRequest', cb, { priority: -10 });
196
314
  *
197
315
  * // Remove subscription
198
316
  * cleanup();
199
317
  */
200
- on<K extends HookName<Lifecycle>>(name: K, cbOrOpts: HookOrOptions<FuncOrNever<Lifecycle[K]>, FailArgs>): () => void;
318
+ add<K extends HookName<Lifecycle>>(name: K, callback: HookCallback<FuncOrNever<Lifecycle[K]>, FailArgs>, options?: HookEngine.AddOptions): () => void;
201
319
  /**
202
- * Subscribe to a lifecycle hook that fires only once.
203
- * Sugar for `on(name, { callback, once: true })`.
320
+ * Run all callbacks for a hook asynchronously.
204
321
  *
205
- * @param name - Name of the lifecycle hook
206
- * @param callback - Callback function
207
- * @returns Cleanup function to remove the subscription
322
+ * @param name - Name of the lifecycle hook to run
323
+ * @param args - Arguments to pass to callbacks (spread + ctx)
324
+ * @returns RunResult with final args, result, and returned flag
208
325
  *
209
326
  * @example
210
- * // Log only the first request
211
- * hooks.once('preRequest', async (ctx) => {
212
- * console.log('First request:', ctx.args[0]);
213
- * });
327
+ * const pre = await hooks.run('beforeRequest', url, options);
328
+ * if (pre.returned) return pre.result;
329
+ * const response = await fetch(...pre.args);
214
330
  */
215
- once<K extends HookName<Lifecycle>>(name: K, callback: HookFn<FuncOrNever<Lifecycle[K]>, FailArgs>): () => void;
331
+ run<K extends HookName<Lifecycle>>(name: K, ...args: Parameters<FuncOrNever<Lifecycle[K]>> | [...Parameters<FuncOrNever<Lifecycle[K]>>, HookEngine.RunOptions<FuncOrNever<Lifecycle[K]>>]): Promise<RunResult<FuncOrNever<Lifecycle[K]>>>;
216
332
  /**
217
- * Emit a lifecycle hook, running all subscribed callbacks.
218
- *
219
- * @param name - Name of the lifecycle hook to emit
220
- * @param args - Arguments to pass to callbacks
221
- * @returns EmitResult with final args, result, and earlyReturn flag
222
- *
223
- * @example
224
- * const result = await hooks.emit('cacheCheck', url);
225
- *
226
- * if (result.earlyReturn && result.result) {
227
- * return result.result; // Use cached value
228
- * }
333
+ * Run all callbacks for a hook synchronously.
229
334
  *
230
- * // Continue with modified args
231
- * const [modifiedUrl] = result.args;
232
- */
233
- emit<K extends HookName<Lifecycle>>(name: K, ...args: Parameters<FuncOrNever<Lifecycle[K]>>): Promise<EmitResult<FuncOrNever<Lifecycle[K]>>>;
234
- /**
235
- * Clear all registered hooks.
335
+ * @param name - Name of the lifecycle hook to run
336
+ * @param args - Arguments to pass to callbacks (spread + ctx)
337
+ * @returns RunResult with final args, result, and returned flag
236
338
  *
237
339
  * @example
238
- * hooks.on('preRequest', validator);
239
- * hooks.on('postRequest', logger);
240
- *
241
- * // Reset for testing
242
- * hooks.clear();
340
+ * const pre = hooks.runSync('beforeValidation', data);
341
+ * if (pre.returned) return pre.result;
243
342
  */
244
- clear(): void;
343
+ runSync<K extends HookName<Lifecycle>>(name: K, ...args: Parameters<FuncOrNever<Lifecycle[K]>> | [...Parameters<FuncOrNever<Lifecycle[K]>>, HookEngine.RunOptions<FuncOrNever<Lifecycle[K]>>]): RunResult<FuncOrNever<Lifecycle[K]>>;
245
344
  /**
246
- * Wrap a function with pre/post lifecycle hooks.
345
+ * Wrap an async function with pre/post lifecycle hooks.
247
346
  *
248
- * - Pre hook: emitted with function args, can modify args or returnEarly with result
249
- * - Post hook: emitted with [result, ...args], can modify result
347
+ * - Pre hook: called with function args, can modify args or return early
348
+ * - Post hook: called with `(result, ...originalArgs)`, can transform result
250
349
  *
251
350
  * @param fn - The async function to wrap
252
351
  * @param hooks - Object with optional pre and post hook names
253
352
  * @returns Wrapped function with same signature
254
353
  *
255
354
  * @example
256
- * interface Lifecycle {
257
- * preRequest(url: string, opts: RequestInit): Promise<Response>;
258
- * postRequest(result: Response, url: string, opts: RequestInit): Promise<Response>;
259
- * }
260
- *
261
- * const hooks = new HookEngine<Lifecycle>();
262
- *
263
- * // Add cache check in pre hook
264
- * hooks.on('preRequest', async (ctx) => {
265
- * const cached = cache.get(ctx.args[0]);
266
- * if (cached) {
267
- * ctx.setResult(cached);
268
- * ctx.returnEarly();
269
- * }
270
- * });
271
- *
272
- * // Log result in post hook
273
- * hooks.on('postRequest', async (ctx) => {
274
- * const [result, url] = ctx.args;
275
- * console.log(`Fetched ${url}:`, result.status);
276
- * });
277
- *
278
- * // Wrap the fetch function
279
355
  * const wrappedFetch = hooks.wrap(
280
356
  * async (url: string, opts: RequestInit) => fetch(url, opts),
281
- * { pre: 'preRequest', post: 'postRequest' }
357
+ * { pre: 'beforeRequest', post: 'afterRequest' }
282
358
  * );
283
359
  */
284
- wrap<F extends AsyncFunc>(fn: F, hooks: {
360
+ wrap<F extends (...args: any[]) => Promise<any>>(fn: F, hooks: {
285
361
  pre: HookName<Lifecycle>;
286
362
  post?: HookName<Lifecycle>;
287
363
  } | {
288
364
  pre?: HookName<Lifecycle>;
289
365
  post: HookName<Lifecycle>;
290
366
  }): (...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>>;
367
+ /**
368
+ * Wrap a synchronous function with pre/post lifecycle hooks.
369
+ *
370
+ * @param fn - The sync function to wrap
371
+ * @param hooks - Object with optional pre and post hook names
372
+ * @returns Wrapped function with same signature
373
+ *
374
+ * @example
375
+ * const wrappedValidate = hooks.wrapSync(
376
+ * (data: UserData) => validate(data),
377
+ * { pre: 'beforeValidate' }
378
+ * );
379
+ */
380
+ wrapSync<F extends (...args: any[]) => any>(fn: F, hooks: {
381
+ pre: HookName<Lifecycle>;
382
+ post?: HookName<Lifecycle>;
383
+ } | {
384
+ pre?: HookName<Lifecycle>;
385
+ post: HookName<Lifecycle>;
386
+ }): (...args: Parameters<F>) => ReturnType<F>;
387
+ /**
388
+ * Execute middleware hooks as an onion (nested) composition.
389
+ *
390
+ * Unlike `run()` which executes hooks linearly, `pipe()` composes hooks
391
+ * as nested middleware. Each hook receives a `next` function that calls
392
+ * the next layer. The innermost layer is `coreFn`. Control flow is
393
+ * managed by calling or not calling `next()` — no `ctx.returns()` needed.
394
+ *
395
+ * Hooks execute in priority order (lower first = outermost layer).
396
+ *
397
+ * @param name - Name of the lifecycle hook
398
+ * @param coreFn - The innermost function to wrap
399
+ * @param args - Arguments passed to each middleware
400
+ * @returns The result from the middleware chain
401
+ *
402
+ * @example
403
+ * // Retry plugin wraps the fetch call
404
+ * hooks.add('execute', async (next, opts, ctx) => {
405
+ * for (let i = 0; i < 3; i++) {
406
+ * const [result, err] = await attempt(next);
407
+ * if (!err) return result;
408
+ * await wait(1000 * i);
409
+ * }
410
+ * throw lastError;
411
+ * }, { priority: -20 });
412
+ *
413
+ * // Dedupe plugin wraps retry
414
+ * hooks.add('execute', async (next, opts, ctx) => {
415
+ * const inflight = getInflight(key);
416
+ * if (inflight) return inflight;
417
+ * const result = await next();
418
+ * share(result);
419
+ * return result;
420
+ * }, { priority: -30 });
421
+ *
422
+ * // Execute: dedupe( retry( makeCall() ) )
423
+ * const response = await hooks.pipe(
424
+ * 'execute',
425
+ * () => makeCall(opts),
426
+ * opts
427
+ * );
428
+ */
429
+ pipe<K extends HookName<Lifecycle>, R = unknown>(name: K, coreFn: () => Promise<R>, ...args: unknown[]): Promise<R>;
430
+ /**
431
+ * Synchronous version of `pipe()`.
432
+ *
433
+ * @param name - Name of the lifecycle hook
434
+ * @param coreFn - The innermost function to wrap
435
+ * @param args - Arguments passed to each middleware
436
+ * @returns The result from the middleware chain
437
+ */
438
+ pipeSync<K extends HookName<Lifecycle>, R = unknown>(name: K, coreFn: () => R, ...args: unknown[]): R;
439
+ /**
440
+ * Clear all hooks and reset registration state.
441
+ *
442
+ * @example
443
+ * hooks.add('beforeRequest', validator);
444
+ * hooks.clear();
445
+ * // All hooks removed, back to permissive mode
446
+ */
447
+ clear(): void;
448
+ }
449
+ export declare namespace HookEngine {
450
+ interface Options<FailArgs extends unknown[] = [string]> {
451
+ /**
452
+ * Custom handler for `ctx.fail()`.
453
+ * Can be an Error constructor or a function that throws.
454
+ *
455
+ * @example
456
+ * new HookEngine({ handleFail: HttpsError });
457
+ */
458
+ handleFail?: HandleFail<FailArgs>;
459
+ }
460
+ interface AddOptions {
461
+ /** Remove after first run (sugar for `times: 1`) */
462
+ once?: true;
463
+ /** Run N times then auto-remove */
464
+ times?: number;
465
+ /** Swallow errors from this callback, continue chain */
466
+ ignoreOnFail?: true;
467
+ /** Execution order, lower runs first. Default 0. */
468
+ priority?: number;
469
+ }
470
+ interface RunOptions<F extends (...args: any[]) => any = (...args: any[]) => any> {
471
+ /** Ephemeral callback that runs last (for per-request hooks) */
472
+ append?: HookCallback<F>;
473
+ /** Shared scope that flows across hook runs and engine instances */
474
+ scope?: HookScope;
475
+ }
476
+ interface PipeOptions {
477
+ /** Ephemeral middleware that runs last (innermost before coreFn) */
478
+ append?: (...args: any[]) => any;
479
+ /** Shared scope that flows across hook runs and engine instances */
480
+ scope?: HookScope;
481
+ }
291
482
  }
292
483
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logosdx/hooks",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0",
4
4
  "description": "A lightweight, type-safe hook system for extending function behavior",
5
5
  "license": "BSD-3-Clause",
6
6
  "homepage": "https://logosdx.dev/",
@@ -38,6 +38,6 @@
38
38
  "unpkg": "./dist/browser/bundle.js",
39
39
  "jsdelivr": "./dist/browser/bundle.js",
40
40
  "dependencies": {
41
- "@logosdx/utils": "^6.1.0-beta.0"
41
+ "@logosdx/utils": "^6.1.0"
42
42
  }
43
43
  }