@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.
- package/CHANGELOG.md +140 -0
- package/dist/browser/bundle.js +1 -1
- package/dist/browser/bundle.js.map +1 -1
- package/dist/cjs/index.js +904 -192
- package/dist/esm/index.mjs +977 -202
- package/dist/types/index.d.ts +361 -170
- package/package.json +2 -2
- package/readme.md +3 -3
- package/src/index.ts +972 -285
package/dist/types/index.d.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FunctionProps } from '@logosdx/utils';
|
|
2
2
|
/**
|
|
3
3
|
* Error thrown when a hook calls `ctx.fail()`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
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.
|
|
10
|
-
* if (!
|
|
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.
|
|
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
|
|
32
|
-
* if (isHookError(
|
|
33
|
-
* console.log(`Hook "${
|
|
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
|
-
*
|
|
36
|
+
* Custom error handler for `ctx.fail()`.
|
|
37
|
+
* Can be an Error constructor or a function that throws.
|
|
39
38
|
*/
|
|
40
|
-
export
|
|
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
|
-
*
|
|
50
|
-
*
|
|
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
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
46
|
+
* // Private plugin state (symbol key)
|
|
47
|
+
* const CACHE_STATE = Symbol('cache');
|
|
48
|
+
* ctx.scope.set(CACHE_STATE, { key, rule });
|
|
56
49
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* ctx.returnEarly();
|
|
60
|
-
* }
|
|
61
|
-
* });
|
|
50
|
+
* // Shared cross-plugin contract (string key)
|
|
51
|
+
* ctx.scope.set('serializedKey', key);
|
|
62
52
|
*/
|
|
63
|
-
export
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
type
|
|
81
|
-
|
|
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
|
-
*
|
|
89
|
-
*
|
|
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
|
|
92
|
+
export type HookName<T> = FunctionProps<T>;
|
|
92
93
|
/**
|
|
93
|
-
*
|
|
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
|
|
114
|
+
export declare class HookContext<Args extends unknown[] = unknown[], Result = unknown, FailArgs extends unknown[] = [string]> {
|
|
96
115
|
/**
|
|
97
|
-
*
|
|
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
|
-
* //
|
|
102
|
-
*
|
|
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
|
-
*
|
|
105
|
-
*
|
|
106
|
-
|
|
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
|
-
|
|
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
|
|
250
|
+
* Callbacks receive spread arguments with a context object as the last param.
|
|
116
251
|
*
|
|
117
252
|
* @example
|
|
118
253
|
* interface FetchLifecycle {
|
|
119
|
-
*
|
|
120
|
-
*
|
|
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.
|
|
127
|
-
*
|
|
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.
|
|
133
|
-
*
|
|
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
|
-
*
|
|
137
|
-
*
|
|
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?:
|
|
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('
|
|
287
|
+
* .register('beforeRequest', 'afterRequest');
|
|
172
288
|
*
|
|
173
|
-
* hooks.
|
|
174
|
-
* hooks.
|
|
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
|
|
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.
|
|
187
|
-
* console.log('Request:',
|
|
303
|
+
* const cleanup = hooks.add('beforeRequest', (url, opts, ctx) => {
|
|
304
|
+
* console.log('Request:', url);
|
|
188
305
|
* });
|
|
189
306
|
*
|
|
190
307
|
* // With options
|
|
191
|
-
* hooks.
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
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
|
-
|
|
318
|
+
add<K extends HookName<Lifecycle>>(name: K, callback: HookCallback<FuncOrNever<Lifecycle[K]>, FailArgs>, options?: HookEngine.AddOptions): () => void;
|
|
201
319
|
/**
|
|
202
|
-
*
|
|
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
|
|
207
|
-
* @returns
|
|
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
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
231
|
-
*
|
|
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.
|
|
239
|
-
*
|
|
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
|
-
|
|
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
|
|
345
|
+
* Wrap an async function with pre/post lifecycle hooks.
|
|
247
346
|
*
|
|
248
|
-
* - Pre hook:
|
|
249
|
-
* - Post hook:
|
|
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: '
|
|
357
|
+
* { pre: 'beforeRequest', post: 'afterRequest' }
|
|
282
358
|
* );
|
|
283
359
|
*/
|
|
284
|
-
wrap<F extends
|
|
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
|
|
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
|
|
41
|
+
"@logosdx/utils": "^6.1.0"
|
|
42
42
|
}
|
|
43
43
|
}
|