@logosdx/hooks 1.0.0-beta.1 → 1.0.0-beta.2
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 +22 -0
- package/LICENSE +1 -1
- package/dist/browser/bundle.js +2 -0
- package/dist/browser/bundle.js.map +1 -0
- package/dist/cjs/index.js +906 -0
- package/dist/esm/index.mjs +910 -0
- package/dist/types/index.d.ts +292 -0
- package/package.json +26 -23
- package/readme.md +84 -0
- package/src/index.ts +565 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @logosdx/hooks
|
|
2
|
+
|
|
3
|
+
## 1.0.0-beta.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [11e8233]
|
|
8
|
+
- @logosdx/utils@6.1.0-beta.0
|
|
9
|
+
|
|
10
|
+
## 1.0.0-beta.0
|
|
11
|
+
|
|
12
|
+
### Major Changes
|
|
13
|
+
|
|
14
|
+
- 99a13ba: Initial beta release of @logosdx/hooks - a lightweight, type-safe hook system for extending function behavior.
|
|
15
|
+
|
|
16
|
+
Features:
|
|
17
|
+
|
|
18
|
+
- `HookEngine` class for wrapping functions with before/after/error extension points
|
|
19
|
+
- `make()` and `wrap()` methods for creating hookable functions
|
|
20
|
+
- Extension options: `once`, `ignoreOnFail`
|
|
21
|
+
- Context methods: `setArgs`, `setResult`, `returnEarly`, `fail`, `removeHook`
|
|
22
|
+
- `HookError` and `isHookError()` for typed error handling
|
package/LICENSE
CHANGED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var L=Object.defineProperty;var $=r=>{throw TypeError(r)};var q=(r,s,o)=>s in r?L(r,s,{enumerable:!0,configurable:!0,writable:!0,value:o}):r[s]=o;var v=(r,s,o)=>q(r,typeof s!="symbol"?s+"":s,o),F=(r,s,o)=>s.has(r)||$("Cannot "+o);var l=(r,s,o)=>(F(r,s,"read from private field"),o?o.call(r):s.get(r)),y=(r,s,o)=>s.has(r)?$("Cannot add the same private member more than once"):s instanceof WeakSet?s.add(r):s.set(r,o),b=(r,s,o,g)=>(F(r,s,"write to private field"),g?g.call(r,o):s.set(r,o),o),E=(r,s,o)=>(F(r,s,"access private method"),o);this.LogosDx=this.LogosDx||{};this.LogosDx.Hooks=function(r){"use strict";var p,d,m,f,w,H;class s extends Error{}const o=(i,e,t)=>{if((i instanceof Function?!!i():!!i)===!1)throw new s(e||"assertion failed")},g=i=>i instanceof Function,A=i=>i instanceof Object,j=async i=>{o(g(i),"fn must be a function");try{return[await i(),null]}catch(e){return[null,e]}},M=i=>{o(g(i),"fn must be a function");try{return[i(),null]}catch(e){return[null,e]}};class k extends Error{constructor(t){super(t);v(this,"hookName");v(this,"originalError")}}const C=i=>i?.constructor?.name===k.name;class D{constructor(e={}){y(this,w);y(this,p,new Map);y(this,d,new WeakMap);y(this,m);y(this,f,null);b(this,m,e.handleFail??(t=>{throw new k(t)}))}register(...e){o(e.length>0,"register() requires at least one hook name"),l(this,f)===null&&b(this,f,new Set);for(const t of e)o(typeof t=="string",`Hook name must be a string, got ${typeof t}`),l(this,f).add(t);return this}on(e,t){const c=typeof t=="function"?t:t?.callback,u=typeof t=="function"?{}:t;o(typeof e=="string",'"name" must be a string'),o(g(c)||A(t),'"cbOrOpts" must be a callback or options'),o(g(c),"callback must be a function"),E(this,w,H).call(this,e,"on");const a=l(this,p).get(e)??new Set;return a.add(c),l(this,p).set(e,a),l(this,d).set(c,u),()=>{a.delete(c)}}once(e,t){return this.on(e,{callback:t,once:!0})}async emit(e,...t){E(this,w,H).call(this,e,"emit");let c=!1;const u=l(this,p).get(e),a={args:t,removeHook(){},returnEarly(){c=!0},setArgs:n=>{o(Array.isArray(n),`setArgs: args for '${String(e)}' must be an array`),a.args=n},setResult:n=>{a.result=n},fail:(...n)=>{const h=l(this,m),S=typeof h=="function"&&h.prototype?.constructor===h,[,R]=M(()=>{if(S)throw new h(...n);h(...n)});throw R?(R instanceof k&&(R.hookName=String(e)),R):new k("ctx.fail() handler did not throw")}};if(!u||u.size===0)return{args:a.args,result:a.result,earlyReturn:!1};for(const n of u){a.removeHook=()=>u.delete(n);const h=l(this,d).get(n)??{},[,S]=await j(()=>n({...a}));if(h.once&&a.removeHook(),S&&h.ignoreOnFail!==!0)throw S;if(c)break}return{args:a.args,result:a.result,earlyReturn:c}}clear(){l(this,p).clear(),b(this,d,new WeakMap),b(this,f,null)}wrap(e,t){return o(t.pre||t.post,'wrap() requires at least one of "pre" or "post" hooks'),t.pre&&E(this,w,H).call(this,t.pre,"wrap"),t.post&&E(this,w,H).call(this,t.post,"wrap"),async(...c)=>{let u=c,a;if(t.pre){const n=await this.emit(t.pre,...u);if(u=n.args,n.earlyReturn&&n.result!==void 0)return n.result}if(a=await e(...u),t.post){const n=await this.emit(t.post,a,...u);if(n.result!==void 0)return n.result}return a}}}return p=new WeakMap,d=new WeakMap,m=new WeakMap,f=new WeakMap,w=new WeakSet,H=function(e,t){if(l(this,f)!==null&&!l(this,f).has(e)){const c=[...l(this,f)].map(String).join(", ");throw new Error(`Hook "${String(e)}" is not registered. Call register("${String(e)}") before using ${t}(). Registered hooks: ${c||"(none)"}`)}},r.HookEngine=D,r.HookError=k,r.isHookError=C,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"}),r}({});
|
|
2
|
+
//# sourceMappingURL=bundle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle.js","sources":["../../../utils/dist/esm/validation/assert.mjs","../../../utils/dist/esm/validation/type-guards.mjs","../../../utils/dist/esm/async/attempt.mjs","../../src/index.ts"],"sourcesContent":["import { reach } from '../object-utils/index.mjs';\n/**\n * Error class for assertions that fail\n */ export class AssertError extends Error {\n}\n/**\n * Checks if an error is an AssertError.\n *\n * @param err error to check\n * @returns true if error is an AssertError\n */ export const isAssertError = (err)=>err?.constructor?.name === AssertError.name;\n/**\n * Optional value check with custom validation.\n *\n * Returns true if value is undefined/null OR if the custom check passes.\n * Useful for validating optional parameters with specific criteria.\n *\n * @param val value to check\n * @param check function or boolean to validate the value\n * @returns true if value is optional or passes the check\n *\n * @example\n * // With function check\n * function processData(data: any, timeout?: number) {\n * assert(isOptional(timeout, (t) => t > 0), 'Timeout must be positive');\n * // Process data...\n * }\n *\n * @example\n * // With boolean check\n * const isValid = validateInput(input);\n * if (isOptional(config.strict, isValid)) {\n * // Either strict mode is off or input is valid\n * processInput(input);\n * }\n *\n * @example\n * // Check optional email format\n * isOptional(user.email, (email) => email.includes('@')) // true if email is undefined or contains @\n */ export const isOptional = (val, check)=>val === undefined || val === null || (check instanceof Function ? !!check(val) : !!check);\n/**\n * Asserts that a value is true. Even though NodeJS has\n * an `assert` builtin library, this aims to bring a\n * single API for asserting across all environments.\n *\n * @param test value that is coerced to true\n * @param message error message to display when test is false\n * @param ErrorClass error class to throw\n *\n * @example\n *\n * ```ts\n * assert(true, 'this is true');\n * assert(false, 'this is false');\n * assert(() => true, 'this is true');\n * assert(() => false, 'this is false');\n * ```\n *\n * ```ts\n *\n * const SomeErrorClass = class extends Error {\n * constructor(message: string) {\n * super(message);\n * }\n * }\n *\n *\n * const someFunc = () => {\n *\n * assert(true, 'this is true', SomeErrorClass);\n * assert(false, 'this is false', SomeErrorClass);\n * assert(() => true, 'this is true', SomeErrorClass);\n * assert(() => false, 'this is false', SomeErrorClass);\n *\n * // some logic\n * }\n *\n * someFunc();\n * ```\n */ export const assert = (test, message, ErrorClass)=>{\n const check = test instanceof Function ? !!test() : !!test;\n if (check === false) {\n throw new (ErrorClass || AssertError)(message || 'assertion failed');\n }\n};\n/**\n * Asserts the values in an object based on the provided assertions.\n * The assertions are a map of paths to functions that return a tuple\n * of a boolean and a message. This is intended to be used for testing\n * and validation when there is no schema validator available.\n *\n *\n * @param obj\n * @param assertions\n *\n * @example\n *\n * const obj = {\n * a: 1,\n * b: 'hello',\n * c: { d: 2 }\n * }\n *\n * assertObject(obj, {\n * a: (val) => [val === 1, 'a should be 1'],\n * b: (val) => [val === 'hello', 'b should be hello'],\n * c: [\n * (val) => [!!val, 'c should not be empty'],\n * (val) => [isObject(val), 'c should be an object']\n * ],\n * 'c.d': (val) => [isOptional(val, v === 2), 'c.d should be 2']\n * });\n */ export const assertObject = (obj, assertions)=>{\n const tests = [];\n for(const path in assertions){\n const val = reach(obj, path);\n const test = assertions[path];\n if (test === undefined) {\n throw new Error(`assertion for path ${path} is undefined`);\n }\n if (test instanceof Array) {\n for (const t of test){\n tests.push([\n val,\n t\n ]);\n }\n continue;\n }\n tests.push([\n val,\n test\n ]);\n }\n for (const [val, test] of tests){\n const res = test(val);\n assert(res instanceof Array, `assertion did not return a tuple [boolean, string]`);\n const [check, message] = res;\n assert(check, message);\n }\n};\n/**\n * Asserts only if value is not undefined.\n *\n * Provides conditional assertion that only executes when the value is defined.\n * Useful for validating optional parameters or properties.\n *\n * @param val value to test\n * @param test assertion test\n * @param message error message\n * @param ErrorClass error class to throw\n *\n * @example\n * function processUser(user: User, options?: ProcessOptions) {\n * // Only assert options if they are provided\n * assertOptional(options, isObject(options), 'Options must be an object');\n *\n * // Process user...\n * }\n *\n * @example\n * const config = getConfig();\n * assertOptional(config.timeout, config.timeout > 0, 'Timeout must be positive');\n */ export const assertOptional = (val, ...rest)=>{\n if (val !== undefined) {\n assert(...rest);\n }\n};\n","/**\n * Checks if value is non-iterable by testing if it can be iterated over.\n *\n * Uses Symbol.iterator to determine if a value is iterable. Returns true\n * for values that cannot be iterated (null, undefined, primitives).\n *\n * @param val value to check for iterability\n * @returns true if value is not iterable, false if it can be iterated\n *\n * @example\n * isNonIterable(null) // true\n * isNonIterable('string') // true\n * isNonIterable([1,2,3]) // false\n * isNonIterable(new Set()) // false\n */ export const isNonIterable = (val)=>{\n // null and undefined are not iterable\n if (val === null || val === undefined) {\n return true;\n }\n // Check if value has Symbol.iterator property\n return !val[Symbol.iterator];\n};\n/**\n * Checks if value is a primitive type.\n *\n * @param val value to check\n * @returns true if value is a primitive type\n *\n * @example\n * isPrimitive(null) // true\n * isPrimitive(undefined) // true\n * isPrimitive('string') // true\n * isPrimitive(1) // true\n * isPrimitive(true) // true\n * isPrimitive(Symbol('symbol')) // true\n * isPrimitive(new Date()) // false\n * isPrimitive(new RegExp('')) // false\n * isPrimitive(new Error('')) // false\n * isPrimitive(new Set()) // false\n * isPrimitive(new Map()) // false\n * isPrimitive(new Array()) // true\n * isPrimitive(new Object()) // true\n */ export const isPrimitive = (val)=>val === null || val === undefined || typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean' || typeof val === 'symbol' || typeof val === 'bigint';\n/**\n * Checks if value is a type that does not have a constructor.\n *\n * Tests for null and undefined values which lack constructors.\n * Useful for deep comparison and cloning operations.\n *\n * @param val value to check\n * @returns true if value has no constructor (null or undefined)\n *\n * @example\n * hasNoConstructor(null) // true\n * hasNoConstructor(undefined) // true\n * hasNoConstructor({}) // false\n * hasNoConstructor('string') // false\n * hasNoConstructor(42) // false\n */ export const hasNoConstructor = (val)=>val === null || val === undefined;\n/**\n * Checks if both values have the same constructor.\n *\n * Compares the constructor property of two values to determine if they\n * are instances of the same class or type.\n *\n * @param value first value to compare\n * @param compare second value to compare\n * @returns true if both values have the same constructor\n *\n * @example\n * hasSameConstructor([], [1, 2, 3]) // true (both Arrays)\n * hasSameConstructor({}, { a: 1 }) // true (both Objects)\n * hasSameConstructor([], {}) // false (Array vs Object)\n * hasSameConstructor(new Date(), new Date()) // true (both Dates)\n * hasSameConstructor('string', 42) // false (String vs Number)\n */ export const hasSameConstructor = (value, compare)=>hasNoConstructor(value) === false && hasNoConstructor(compare) === false && value.constructor === compare.constructor;\n/**\n * Checks if both values have the same length or size.\n *\n * Compares the length property for arrays or size property for Sets.\n * Both iterables must be the same data type (both arrays or both Sets).\n * Useful for validating collections before performing operations.\n *\n * @param a first collection (array or Set)\n * @param b second collection (array or Set) - must be same type as `a`\n * @returns true if both collections have the same length/size\n *\n * @example\n * isSameLength([1, 2, 3], ['a', 'b', 'c']) // true\n * isSameLength([1, 2], [1, 2, 3]) // false\n * isSameLength(new Set([1, 2]), new Set(['a', 'b'])) // true\n * isSameLength(new Set([1, 2]), new Set([1, 2, 3])) // false\n */ export const isSameLength = (a, b)=>a.length === b.length && a.size === b.size;\n/**\n * Checks if value is a function.\n *\n * Uses instanceof to test if the value is a Function.\n * More reliable than typeof for all function types.\n *\n * @param a value to check\n * @returns true if value is a function\n *\n * @example\n * isFunction(() => {}) // true\n * isFunction(function() {}) // true\n * isFunction(async () => {}) // true\n * isFunction(class MyClass {}) // true\n * isFunction('string') // false\n * isFunction({}) // false\n */ export const isFunction = (a)=>a instanceof Function;\n/**\n * Checks if value is an object.\n *\n * Uses instanceof to test if the value is an Object.\n * Returns true for objects, arrays, functions, dates, etc.\n *\n * @param a value to check\n * @returns true if value is an object\n *\n * @example\n * isObject({}) // true\n * isObject([]) // true\n * isObject(new Date()) // true\n * isObject(() => {}) // true\n * isObject('string') // false\n * isObject(null) // false\n */ export const isObject = (a)=>a instanceof Object;\nconst commonObjects = new Set([\n Date,\n RegExp,\n Function,\n Error,\n EvalError,\n RangeError,\n ReferenceError,\n SyntaxError,\n TypeError,\n URIError,\n AggregateError,\n DOMException,\n Array,\n Set,\n Map,\n WeakMap,\n WeakSet,\n Promise,\n Proxy,\n Symbol,\n BigInt,\n WeakRef,\n FinalizationRegistry,\n DataView,\n ArrayBuffer,\n Int8Array,\n Uint8Array,\n Uint8ClampedArray,\n Int16Array,\n Uint16Array,\n Int32Array,\n Uint32Array,\n Float32Array,\n Float64Array,\n BigInt64Array,\n BigUint64Array,\n FormData,\n URLSearchParams\n]);\n/**\n * Checks if value is an uncommon object. Used to determine if a value\n * is a user defined object.\n *\n * @param a value to check\n * @returns true if value is an uncommon object\n *\n * @example\n *\n * // Returns false for common objects\n * isPlainObject(new Date()) // false\n * isPlainObject(new RegExp('')) // false\n * isPlainObject(new Function()) // false\n * isPlainObject(new Error()) // false\n * isPlainObject(new Array()) // false\n * isPlainObject(new Set()) // false\n *\n * // Returns true for uncommon objects\n * isPlainObject({}) // true\n * isPlainObject(new MyClass()) // true\n *\n */ export const isPlainObject = (a)=>!isPrimitive(a) && isObject(a) && !commonObjects.has(a.constructor);\n/**\n * Checks if value is specifically undefined.\n *\n * Strict equality check for undefined values.\n * More explicit than checking truthiness.\n *\n * @param val value to check\n * @returns true if value is exactly undefined\n *\n * @example\n * isUndefined(undefined) // true\n * isUndefined(null) // false\n * isUndefined('') // false\n * isUndefined(0) // false\n *\n * let x;\n * isUndefined(x) // true\n */ export const isUndefined = (val)=>val === undefined;\n/**\n * Checks if value is specifically not undefined.\n *\n * Inverse of isUndefined. Returns true for all values except undefined,\n * including null, false, 0, and empty strings.\n *\n * @param val value to check\n * @returns true if value is not undefined\n *\n * @example\n * isDefined(null) // true\n * isDefined(0) // true\n * isDefined('') // true\n * isDefined(false) // true\n * isDefined(undefined) // false\n *\n * const config = getConfig();\n * if (isDefined(config.apiKey)) {\n * // Safe to use config.apiKey\n * }\n */ export const isDefined = (val)=>val !== undefined;\n/**\n * Checks if value is specifically null.\n *\n * Strict equality check for null values.\n * More explicit than checking truthiness.\n *\n * @param val value to check\n * @returns true if value is exactly null\n *\n * @example\n * isNull(null) // true\n * isNull(undefined) // false\n * isNull('') // false\n * isNull(0) // false\n *\n * const result = findUser(id);\n * if (isNull(result)) {\n * // User was explicitly not found\n * }\n */ export const isNull = (val)=>val === null;\n","import { assert, isFunction } from '../validation/index.mjs';\n/**\n * Error tuple, go-style.\n *\n * @param fn async function to run\n *\n * @example\n *\n * const [result, error] = await attempt(async () => {\n * return 'hello';\n * });\n *\n * if (error) {\n * console.error(error);\n * }\n *\n * console.log(result);\n */ export const attempt = async (fn)=>{\n assert(isFunction(fn), 'fn must be a function');\n try {\n return [\n await fn(),\n null\n ];\n } catch (e) {\n return [\n null,\n e\n ];\n }\n};\n/**\n * Synchronous error tuple, go-style.\n *\n * @example\n *\n * const [result, error] = attemptSync(() => {\n * return 'hello';\n * });\n *\n * if (error) {\n * console.error(error);\n * }\n *\n * console.log(result);\n */ export const attemptSync = (fn)=>{\n assert(isFunction(fn), 'fn must be a function');\n try {\n return [\n fn(),\n null\n ];\n } catch (e) {\n return [\n null,\n e\n ];\n }\n};\n","import {\n assert,\n AsyncFunc,\n attempt,\n attemptSync,\n FunctionProps,\n isFunction,\n isObject\n} from '@logosdx/utils';\n\n/**\n * Error thrown when a hook calls `ctx.fail()`.\n *\n * This error is only created when using the default `handleFail` behavior.\n * If a custom `handleFail` is provided, that error type is thrown instead.\n *\n * @example\n * hooks.on('validate', async (ctx) => {\n * if (!ctx.args[0].isValid) {\n * ctx.fail('Validation failed');\n * }\n * });\n *\n * const [, err] = await attempt(() => engine.emit('validate', data));\n * if (isHookError(err)) {\n * console.log(err.hookName); // 'validate'\n * }\n */\nexport class HookError extends Error {\n\n /** Name of the hook where the error occurred */\n hookName?: string;\n\n /** Original error if `fail()` was called with an Error instance */\n originalError?: Error;\n\n constructor(message: string) {\n\n super(message)\n }\n}\n\n/**\n * Type guard to check if an error is a HookError.\n *\n * @example\n * const { error } = await engine.emit('validate', data);\n * if (isHookError(error)) {\n * console.log(`Hook \"${error.hookName}\" failed`);\n * }\n */\nexport const isHookError = (error: unknown): error is HookError => {\n\n return (error as HookError)?.constructor?.name === HookError.name\n}\n\n/**\n * Result returned from `emit()` after running all hook callbacks.\n */\nexport interface EmitResult<F extends AsyncFunc> {\n\n /** Current arguments (possibly modified by callbacks) */\n args: Parameters<F>;\n\n /** Result value (if set by a callback) */\n result?: Awaited<ReturnType<F>> | undefined;\n\n /** Whether a callback called `returnEarly()` */\n earlyReturn: boolean;\n}\n\n/**\n * Context object passed to hook callbacks.\n * Provides access to arguments, results, and control methods.\n *\n * @example\n * hooks.on('cacheCheck', async (ctx) => {\n * const [url] = ctx.args;\n * const cached = cache.get(url);\n *\n * if (cached) {\n * ctx.setResult(cached);\n * ctx.returnEarly();\n * }\n * });\n */\nexport interface HookContext<F extends AsyncFunc, FailArgs extends unknown[] = [string]> {\n\n /** Current arguments passed to emit() */\n args: Parameters<F>;\n\n /** Result value (can be set by callbacks) */\n result?: Awaited<ReturnType<F>>;\n\n /** Abort hook execution with an error. */\n fail: (...args: FailArgs) => never;\n\n /** Replace the arguments for subsequent callbacks */\n setArgs: (next: Parameters<F>) => void;\n\n /** Set the result value */\n setResult: (next: Awaited<ReturnType<F>>) => void;\n\n /** Stop processing remaining callbacks and return early */\n returnEarly: () => void;\n\n /** Remove this callback from the hook */\n removeHook: () => void;\n}\n\nexport type HookFn<F extends AsyncFunc, FailArgs extends unknown[] = [string]> =\n (ctx: HookContext<F, FailArgs>) => Promise<void>;\n\ntype HookOptions<F extends AsyncFunc, FailArgs extends unknown[] = [string]> = {\n callback: HookFn<F, FailArgs>;\n once?: true;\n ignoreOnFail?: true;\n}\n\ntype HookOrOptions<F extends AsyncFunc, FailArgs extends unknown[] = [string]> =\n HookFn<F, FailArgs> | HookOptions<F, FailArgs>;\n\ntype FuncOrNever<T> = T extends AsyncFunc ? T : never;\n\n/**\n * Custom error handler for `ctx.fail()`.\n * Can be an Error constructor or a function that throws.\n */\nexport type HandleFail<Args extends unknown[] = [string]> =\n | (new (...args: Args) => Error)\n | ((...args: Args) => never);\n\n/**\n * Options for HookEngine constructor.\n */\nexport interface HookEngineOptions<FailArgs extends unknown[] = [string]> {\n\n /**\n * Custom handler for `ctx.fail()`.\n * Can be an Error constructor or a function that throws.\n *\n * @example\n * // Use Firebase HttpsError\n * new HookEngine({ handleFail: HttpsError });\n *\n * // Use custom function\n * new HookEngine({\n * handleFail: (msg, data) => { throw Boom.badRequest(msg, data); }\n * });\n */\n handleFail?: HandleFail<FailArgs>;\n}\n\n/**\n * A lightweight, type-safe lifecycle hook system.\n *\n * HookEngine allows you to define lifecycle events and subscribe to them.\n * Callbacks can modify arguments, set results, or abort execution.\n *\n * @example\n * interface FetchLifecycle {\n * preRequest(url: string, options: RequestInit): Promise<Response>;\n * rateLimit(error: Error, attempt: number): Promise<void>;\n * cacheHit(url: string, data: unknown): Promise<unknown>;\n * }\n *\n * const hooks = new HookEngine<FetchLifecycle>();\n *\n * hooks.on('rateLimit', async (ctx) => {\n * const [error, attempt] = ctx.args;\n * if (attempt > 3) ctx.fail('Max retries exceeded');\n * await sleep(error.retryAfter * 1000);\n * });\n *\n * hooks.on('cacheHit', async (ctx) => {\n * console.log('Cache hit for:', ctx.args[0]);\n * });\n *\n * // In your implementation\n * const result = await hooks.emit('cacheHit', url, cachedData);\n *\n * @typeParam Lifecycle - Interface defining the lifecycle hooks\n * @typeParam FailArgs - Arguments type for ctx.fail() (default: [string])\n */\n/**\n * Default permissive lifecycle type when no type parameter is provided.\n */\ntype DefaultLifecycle = Record<string, AsyncFunc>;\n\n/**\n * Extract only function property keys from a type.\n * This ensures only methods are available as hook names, not data properties.\n *\n * @example\n * interface Doc {\n * id: string;\n * save(): Promise<void>;\n * delete(): Promise<void>;\n * }\n *\n * type DocHooks = HookName<Doc>; // 'save' | 'delete' (excludes 'id')\n */\nexport type HookName<T> = FunctionProps<T>;\n\nexport class HookEngine<Lifecycle = DefaultLifecycle, FailArgs extends unknown[] = [string]> {\n\n #hooks: Map<HookName<Lifecycle>, Set<HookFn<FuncOrNever<Lifecycle[HookName<Lifecycle>]>, FailArgs>>> = new Map();\n #hookOpts = new WeakMap<HookFn<any, any>, HookOptions<any, any>>();\n #handleFail: HandleFail<FailArgs>;\n #registered: Set<HookName<Lifecycle>> | null = null;\n\n constructor(options: HookEngineOptions<FailArgs> = {}) {\n\n this.#handleFail = options.handleFail ?? ((message: string): never => {\n\n throw new HookError(message);\n }) as unknown as HandleFail<FailArgs>;\n }\n\n /**\n * Validate that a hook is registered (if registration is enabled).\n */\n #assertRegistered(name: HookName<Lifecycle>, method: string) {\n\n if (this.#registered !== null && !this.#registered.has(name)) {\n\n const registered = [...this.#registered].map(String).join(', ');\n throw new Error(\n `Hook \"${String(name)}\" is not registered. ` +\n `Call register(\"${String(name)}\") before using ${method}(). ` +\n `Registered hooks: ${registered || '(none)'}`\n );\n }\n }\n\n /**\n * Register hook names for runtime validation.\n * Once any hooks are registered, all hooks must be registered before use.\n *\n * @param names - Hook names to register\n * @returns this (for chaining)\n *\n * @example\n * const hooks = new HookEngine<FetchLifecycle>()\n * .register('preRequest', 'postRequest', 'rateLimit');\n *\n * hooks.on('preRequest', cb); // OK\n * hooks.on('preRequset', cb); // Error: not registered (typo caught!)\n */\n register(...names: HookName<Lifecycle>[]) {\n\n assert(names.length > 0, 'register() requires at least one hook name');\n\n if (this.#registered === null) {\n\n this.#registered = new Set();\n }\n\n for (const name of names) {\n\n assert(typeof name === 'string', `Hook name must be a string, got ${typeof name}`);\n this.#registered.add(name);\n }\n\n return this;\n }\n\n /**\n * Subscribe to a lifecycle hook.\n *\n * @param name - Name of the lifecycle hook\n * @param cbOrOpts - Callback function or options object\n * @returns Cleanup function to remove the subscription\n *\n * @example\n * // Simple callback\n * const cleanup = hooks.on('preRequest', async (ctx) => {\n * console.log('Request:', ctx.args[0]);\n * });\n *\n * // With options\n * hooks.on('analytics', {\n * callback: async (ctx) => { track(ctx.args); },\n * once: true, // Remove after first run\n * ignoreOnFail: true // Don't throw if callback fails\n * });\n *\n * // Remove subscription\n * cleanup();\n */\n on<K extends HookName<Lifecycle>>(\n name: K,\n cbOrOpts: HookOrOptions<FuncOrNever<Lifecycle[K]>, FailArgs>\n ) {\n\n const callback = typeof cbOrOpts === 'function' ? cbOrOpts : cbOrOpts?.callback;\n const opts = typeof cbOrOpts === 'function'\n ? {} as HookOptions<FuncOrNever<Lifecycle[K]>, FailArgs>\n : cbOrOpts;\n\n assert(typeof name === 'string', '\"name\" must be a string');\n assert(isFunction(callback) || isObject(cbOrOpts), '\"cbOrOpts\" must be a callback or options');\n assert(isFunction(callback), 'callback must be a function');\n\n this.#assertRegistered(name, 'on');\n\n const hooks = this.#hooks.get(name) ?? new Set();\n\n hooks.add(callback as HookFn<FuncOrNever<Lifecycle[keyof Lifecycle]>, FailArgs>);\n\n this.#hooks.set(name, hooks);\n this.#hookOpts.set(callback, opts);\n\n return () => {\n\n hooks.delete(callback as HookFn<FuncOrNever<Lifecycle[keyof Lifecycle]>, FailArgs>);\n }\n }\n\n /**\n * Subscribe to a lifecycle hook that fires only once.\n * Sugar for `on(name, { callback, once: true })`.\n *\n * @param name - Name of the lifecycle hook\n * @param callback - Callback function\n * @returns Cleanup function to remove the subscription\n *\n * @example\n * // Log only the first request\n * hooks.once('preRequest', async (ctx) => {\n * console.log('First request:', ctx.args[0]);\n * });\n */\n once<K extends HookName<Lifecycle>>(\n name: K,\n callback: HookFn<FuncOrNever<Lifecycle[K]>, FailArgs>\n ) {\n\n return this.on(name, { callback, once: true });\n }\n\n /**\n * Emit a lifecycle hook, running all subscribed callbacks.\n *\n * @param name - Name of the lifecycle hook to emit\n * @param args - Arguments to pass to callbacks\n * @returns EmitResult with final args, result, and earlyReturn flag\n *\n * @example\n * const result = await hooks.emit('cacheCheck', url);\n *\n * if (result.earlyReturn && result.result) {\n * return result.result; // Use cached value\n * }\n *\n * // Continue with modified args\n * const [modifiedUrl] = result.args;\n */\n async emit<K extends HookName<Lifecycle>>(\n name: K,\n ...args: Parameters<FuncOrNever<Lifecycle[K]>>\n ): Promise<EmitResult<FuncOrNever<Lifecycle[K]>>> {\n\n this.#assertRegistered(name, 'emit');\n\n let earlyReturn = false;\n\n const hooks = this.#hooks.get(name);\n\n const context: HookContext<FuncOrNever<Lifecycle[K]>, FailArgs> = {\n args,\n removeHook() {},\n returnEarly() {\n\n earlyReturn = true;\n },\n setArgs: (next) => {\n\n assert(\n Array.isArray(next),\n `setArgs: args for '${String(name)}' must be an array`\n );\n\n context.args = next;\n },\n setResult: (next) => {\n\n context.result = next;\n },\n fail: ((...failArgs: FailArgs) => {\n\n const handler = this.#handleFail;\n\n // Check if handler is a constructor (class or function with prototype)\n const isConstructor = typeof handler === 'function' &&\n handler.prototype?.constructor === handler;\n\n const [, error] = attemptSync(() => {\n\n if (isConstructor) {\n\n throw new (handler as new (...args: FailArgs) => Error)(...failArgs);\n }\n\n (handler as (...args: FailArgs) => never)(...failArgs);\n });\n\n if (error) {\n\n if (error instanceof HookError) {\n\n error.hookName = String(name);\n }\n\n throw error;\n }\n\n // If handler didn't throw, we need to throw something\n throw new HookError('ctx.fail() handler did not throw');\n }) as (...args: FailArgs) => never\n };\n\n if (!hooks || hooks.size === 0) {\n\n return {\n args: context.args,\n result: context.result,\n earlyReturn: false\n };\n }\n\n for (const fn of hooks) {\n\n context.removeHook = () => hooks.delete(fn as any);\n\n const opts: HookOptions<any, any> = this.#hookOpts.get(fn) ?? { callback: fn };\n const [, err] = await attempt(() => fn({ ...context } as any));\n\n if (opts.once) context.removeHook();\n\n if (err && opts.ignoreOnFail !== true) {\n\n throw err;\n }\n\n if (earlyReturn) break;\n }\n\n return {\n args: context.args,\n result: context.result,\n earlyReturn\n };\n }\n\n /**\n * Clear all registered hooks.\n *\n * @example\n * hooks.on('preRequest', validator);\n * hooks.on('postRequest', logger);\n *\n * // Reset for testing\n * hooks.clear();\n */\n clear() {\n\n this.#hooks.clear();\n this.#hookOpts = new WeakMap();\n this.#registered = null;\n }\n\n /**\n * Wrap a function with pre/post lifecycle hooks.\n *\n * - Pre hook: emitted with function args, can modify args or returnEarly with result\n * - Post hook: emitted with [result, ...args], can modify result\n *\n * @param fn - The async function to wrap\n * @param hooks - Object with optional pre and post hook names\n * @returns Wrapped function with same signature\n *\n * @example\n * interface Lifecycle {\n * preRequest(url: string, opts: RequestInit): Promise<Response>;\n * postRequest(result: Response, url: string, opts: RequestInit): Promise<Response>;\n * }\n *\n * const hooks = new HookEngine<Lifecycle>();\n *\n * // Add cache check in pre hook\n * hooks.on('preRequest', async (ctx) => {\n * const cached = cache.get(ctx.args[0]);\n * if (cached) {\n * ctx.setResult(cached);\n * ctx.returnEarly();\n * }\n * });\n *\n * // Log result in post hook\n * hooks.on('postRequest', async (ctx) => {\n * const [result, url] = ctx.args;\n * console.log(`Fetched ${url}:`, result.status);\n * });\n *\n * // Wrap the fetch function\n * const wrappedFetch = hooks.wrap(\n * async (url: string, opts: RequestInit) => fetch(url, opts),\n * { pre: 'preRequest', post: 'postRequest' }\n * );\n */\n wrap<F extends AsyncFunc>(\n fn: F,\n hooks:\n | { pre: HookName<Lifecycle>; post?: HookName<Lifecycle> }\n | { pre?: HookName<Lifecycle>; post: HookName<Lifecycle> }\n ): (...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>> {\n\n assert(\n hooks.pre || hooks.post,\n 'wrap() requires at least one of \"pre\" or \"post\" hooks'\n );\n\n if (hooks.pre) this.#assertRegistered(hooks.pre, 'wrap');\n if (hooks.post) this.#assertRegistered(hooks.post, 'wrap');\n\n return async (...args: Parameters<F>): Promise<Awaited<ReturnType<F>>> => {\n\n let currentArgs = args;\n let result: Awaited<ReturnType<F>> | undefined;\n\n // Pre hook\n if (hooks.pre) {\n\n const preResult = await this.emit(hooks.pre, ...currentArgs as any);\n\n currentArgs = preResult.args as Parameters<F>;\n\n if (preResult.earlyReturn && preResult.result !== undefined) {\n\n return preResult.result as Awaited<ReturnType<F>>;\n }\n }\n\n // Execute function\n result = await fn(...currentArgs);\n\n // Post hook\n if (hooks.post) {\n\n const postResult = await this.emit(\n hooks.post,\n ...[result, ...currentArgs] as any\n );\n\n if (postResult.result !== undefined) {\n\n return postResult.result as Awaited<ReturnType<F>>;\n }\n }\n\n return result as Awaited<ReturnType<F>>;\n };\n }\n}\n"],"names":["AssertError","assert","test","message","ErrorClass","isFunction","a","isObject","attempt","fn","attemptSync","HookError","__publicField","isHookError","error","HookEngine","options","__privateAdd","_HookEngine_instances","_hooks","_hookOpts","_handleFail","_registered","__privateSet","names","__privateGet","name","cbOrOpts","callback","opts","__privateMethod","assertRegistered_fn","hooks","args","earlyReturn","context","next","failArgs","handler","isConstructor","err","currentArgs","result","preResult","postResult","method","registered"],"mappings":"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"}
|