@logosdx/hooks 1.0.0-beta.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 ADDED
@@ -0,0 +1,15 @@
1
+ # @logosdx/hooks
2
+
3
+ ## 1.0.0-beta.0
4
+
5
+ ### Major Changes
6
+
7
+ - 99a13ba: Initial beta release of @logosdx/hooks - a lightweight, type-safe hook system for extending function behavior.
8
+
9
+ Features:
10
+
11
+ - `HookEngine` class for wrapping functions with before/after/error extension points
12
+ - `make()` and `wrap()` methods for creating hookable functions
13
+ - Extension options: `once`, `ignoreOnFail`
14
+ - Context methods: `setArgs`, `setResult`, `returnEarly`, `fail`, `removeHook`
15
+ - `HookError` and `isHookError()` for typed error handling
package/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ Copyright 2025 LogosDX contributors
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12
+
@@ -0,0 +1,2 @@
1
+ var B=Object.defineProperty;var M=r=>{throw TypeError(r)};var G=(r,e,t)=>e in r?B(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var f=(r,e,t)=>G(r,typeof e!="symbol"?e+"":e,t),D=(r,e,t)=>e.has(r)||M("Cannot "+t);var i=(r,e,t)=>(D(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?M("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),L=(r,e,t,h)=>(D(r,e,"write to private field"),h?h.call(r,t):e.set(r,t),t);this.LogosDx=this.LogosDx||{};this.LogosDx.Hooks=function(r){"use strict";var k,g,b,d;class e extends Error{}const t=(n,o,s)=>{if((n instanceof Function?!!n():!!n)===!1)throw new e(o||"assertion failed")},h=n=>n instanceof Function,E=n=>n instanceof Object,$=async n=>{t(h(n),"fn must be a function");try{return[await n(),null]}catch(o){return[null,o]}};class H extends Error{constructor(s){super(s);f(this,"hookName");f(this,"extPoint");f(this,"originalError");f(this,"aborted",!1)}}const T=n=>n?.constructor?.name===H.name;class j{constructor(){f(this,"before",new Set);f(this,"after",new Set);f(this,"error",new Set)}}const W=new Set(["before","after","error"]);class N{constructor(){y(this,k,new Set);y(this,g,new Map);y(this,b,new WeakMap);y(this,d,new WeakMap)}extend(o,s,c){const u=typeof c=="function"?c:c?.callback,m=typeof c=="function"?{}:c;t(typeof o=="string",'"name" must be a string'),t(i(this,k).has(o),`'${o.toString()}' is not a registered hook`),t(typeof s=="string",'"extensionPoint" must be a string'),t(W.has(s),`'${s}' is not a valid extension point`),t(h(u)||E(c),'"cbOrOpts" must be a extension callback or options'),t(h(u),"callback must be a function");const w=i(this,g).get(o)??new j;return w[s].add(u),i(this,g).set(o,w),i(this,b).set(u,m),()=>{w[s].delete(u)}}make(o,s,c={}){return t(typeof o=="string",'"name" must be a string'),t(!i(this,k).has(o),`'${o.toString()}' hook is already registered`),t(h(s),'"cb" must be a function'),t(E(c),'"opts" must be an object'),i(this,k).add(o),i(this,d).has(s)?i(this,d).get(s):async(...m)=>{let w=!1;const C=i(this,g).get(o),a={args:m,point:"before",removeHook(){},returnEarly(){w=!0},setArgs(l){t(Array.isArray(l),`setArgs: next args for '${a.point}' '${o.toString()}' must be an array of arguments`),a.args=l},setResult(l){a.results=l},fail(l){const p=new H(`Hook Aborted: ${l??"unknown"}`);throw l instanceof Error&&(p.originalError=l),p.extPoint=a.point,p.hookName=o,p}},{before:P,after:R,error:q}=C??new j,S=async(l,p)=>{a.point=p;for(const F of l){a.removeHook=()=>l.delete(F);const v=i(this,b).get(F),[,x]=await $(()=>F({...a}));if(v.once&&a.removeHook(),x&&v.ignoreOnFail!==!0)throw x;if(w)break}};if(await S(P,"before"),w)return a.results;const[z,A]=await $(()=>s.apply(c?.bindTo||s,a.args));if(a.results=z,a.error=A,A)throw a.point="error",await S(q,"error"),A;return await S(R,"after"),a.results}}wrap(o,s,c){t(E(o),'"instance" must be an object');const u=this.make(s,o[s],{bindTo:o,...c});i(this,d).set(u,o[s]),o[s]=u}clear(){i(this,k).clear(),i(this,g).clear(),L(this,b,new WeakMap)}}return k=new WeakMap,g=new WeakMap,b=new WeakMap,d=new WeakMap,r.HookEngine=N,r.HookError=H,r.isHookError=T,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 isFunction,\n isObject,\n FunctionProps\n} from '@logosdx/utils';\n\n/**\n * Error thrown when a hook extension calls `fail()` or when hook execution fails.\n *\n * @example\n * engine.extend('save', 'before', async (ctx) => {\n * if (!ctx.args[0].isValid) {\n * ctx.fail('Validation failed');\n * }\n * });\n *\n * const [, err] = await attempt(() => app.save(data));\n * if (isHookError(err)) {\n * console.log(err.hookName); // 'save'\n * console.log(err.extPoint); // 'before'\n * }\n */\nexport class HookError extends Error {\n\n /** Name of the hook where the error occurred */\n hookName?: string;\n\n /** Extension point where the error occurred: 'before', 'after', or 'error' */\n extPoint?: string;\n\n /** Original error if `fail()` was called with an Error instance */\n originalError?: Error;\n\n /** Whether the hook was explicitly aborted via `fail()` */\n aborted = false;\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 [result, err] = await attempt(() => app.save(data));\n * if (isHookError(err)) {\n * console.log(`Hook \"${err.hookName}\" failed at \"${err.extPoint}\"`);\n * }\n */\nexport const isHookError = (error: unknown): error is HookError => {\n\n return (error as HookError)?.constructor?.name === HookError.name\n}\n\ninterface HookShape<F extends AsyncFunc> {\n args: Parameters<F>,\n results?: Awaited<ReturnType<F>>\n}\n\n/**\n * Context object passed to hook extension callbacks.\n * Provides access to arguments, results, and control methods.\n *\n * @example\n * engine.extend('fetch', 'before', async (ctx) => {\n * // Read current arguments\n * const [url, options] = ctx.args;\n *\n * // Modify arguments before the original function runs\n * ctx.setArgs([url, { ...options, cache: 'force-cache' }]);\n *\n * // Or skip the original function entirely\n * if (isCached(url)) {\n * ctx.setResult(getCached(url));\n * ctx.returnEarly();\n * }\n * });\n */\nexport interface HookContext<F extends AsyncFunc> extends HookShape<F> {\n\n /** Current extension point: 'before', 'after', or 'error' */\n point: keyof Hook<F>;\n\n /** Error from the original function (only set in 'error' extensions) */\n error?: unknown,\n\n /** Abort hook execution with an error. Throws a HookError. */\n fail: (error?: unknown) => never,\n\n /** Replace the arguments passed to the original function */\n setArgs: (next: Parameters<F>) => void,\n\n /** Replace the result returned from the hook chain */\n setResult: (next: Awaited<ReturnType<F>>) => void,\n\n /** Skip the original function and return early with the current result */\n returnEarly: () => void;\n\n /** Remove this extension from the hook (useful with `once` behavior) */\n removeHook: () => void;\n}\n\nexport type HookFn<F extends AsyncFunc> = (ctx: HookContext<F>) => Promise<void>;\n\nclass Hook<F extends AsyncFunc> {\n before: Set<HookFn<F>> = new Set();\n after: Set<HookFn<F>> = new Set();\n error: Set<HookFn<F>> = new Set();\n}\n\nconst allowedExtPoints = new Set([\n 'before',\n 'after',\n 'error'\n]);\n\ntype HookExtOptions<F extends AsyncFunc> = {\n callback: HookFn<F>,\n once?: true,\n ignoreOnFail?: true\n}\n\ntype HookExtOrOptions<F extends AsyncFunc> = HookFn<F> | HookExtOptions<F>\n\ntype MakeHookOptions = {\n bindTo?: any\n}\n\ntype FuncOrNever<T> = T extends AsyncFunc ? T : never;\n\n/**\n * A lightweight, type-safe hook system for extending function behavior.\n *\n * HookEngine allows you to wrap functions and add extensions that run\n * before, after, or on error. Extensions can modify arguments, change\n * results, or abort execution entirely.\n *\n * @example\n * interface MyApp {\n * save(data: Data): Promise<Result>;\n * load(id: string): Promise<Data>;\n * }\n *\n * const app = new MyAppImpl();\n * const hooks = new HookEngine<MyApp>();\n *\n * // Wrap a method to make it hookable\n * hooks.wrap(app, 'save');\n *\n * // Add a validation extension\n * hooks.extend('save', 'before', async (ctx) => {\n * if (!ctx.args[0].isValid) {\n * ctx.fail('Validation failed');\n * }\n * });\n *\n * // Add logging extension\n * hooks.extend('save', 'after', async (ctx) => {\n * console.log('Saved:', ctx.results);\n * });\n *\n * @typeParam Shape - Interface defining the hookable functions\n */\nexport class HookEngine<Shape> {\n\n #registered = new Set<keyof Shape>();\n #hooks: Map<keyof Shape, Hook<FuncOrNever<Shape[keyof Shape]>>> = new Map();\n #hookFnOpts = new WeakMap();\n #wrapped = new WeakMap();\n\n /**\n * Add an extension to a registered hook.\n *\n * Extensions run at specific points in the hook lifecycle:\n * - `before`: Runs before the original function. Can modify args or return early.\n * - `after`: Runs after successful execution. Can modify the result.\n * - `error`: Runs when the original function throws. Can handle or transform errors.\n *\n * @param name - Name of the registered hook to extend\n * @param extensionPoint - When to run: 'before', 'after', or 'error'\n * @param cbOrOpts - Extension callback or options object\n * @returns Cleanup function to remove the extension\n *\n * @example\n * // Simple callback\n * const cleanup = hooks.extend('save', 'before', async (ctx) => {\n * console.log('About to save:', ctx.args);\n * });\n *\n * // With options\n * hooks.extend('save', 'after', {\n * callback: async (ctx) => { console.log('Saved!'); },\n * once: true, // Remove after first run\n * ignoreOnFail: true // Don't throw if this extension fails\n * });\n *\n * // Later: remove the extension\n * cleanup();\n */\n extend<K extends FunctionProps<Shape>>(\n name: K,\n extensionPoint: keyof Hook<FuncOrNever<Shape[K]>>,\n cbOrOpts: HookExtOrOptions<FuncOrNever<Shape[K]>>\n ) {\n const callback = typeof cbOrOpts === 'function' ? cbOrOpts : cbOrOpts?.callback;\n const opts = typeof cbOrOpts === 'function' ? {} as HookExtOptions<FuncOrNever<Shape[K]>> : cbOrOpts;\n\n assert(typeof name === 'string', '\"name\" must be a string');\n assert(this.#registered.has(name), `'${name.toString()}' is not a registered hook`);\n assert(typeof extensionPoint === 'string', '\"extensionPoint\" must be a string');\n assert(allowedExtPoints.has(extensionPoint), `'${extensionPoint}' is not a valid extension point`);\n assert(isFunction(callback) || isObject(cbOrOpts), '\"cbOrOpts\" must be a extension callback or options');\n assert(isFunction(callback), 'callback must be a function');\n\n const hook = this.#hooks.get(name) ?? new Hook<FuncOrNever<Shape[K]>>();\n\n hook[extensionPoint].add(callback);\n\n this.#hooks.set(name, hook);\n this.#hookFnOpts.set(callback, opts);\n\n /**\n * Removes the registered hook extension\n */\n return () => {\n\n hook[extensionPoint].delete(callback);\n }\n }\n\n /**\n * Register a function as a hookable and return the wrapped version.\n *\n * The wrapped function behaves identically to the original but allows\n * extensions to be added via `extend()`. Use `wrap()` for a simpler API\n * when working with object methods.\n *\n * @param name - Unique name for this hook (must match a key in Shape)\n * @param cb - The original function to wrap\n * @param opts - Options for the wrapped function\n * @returns Wrapped function with hook support\n *\n * @example\n * const hooks = new HookEngine<{ fetch: typeof fetch }>();\n *\n * const hookedFetch = hooks.make('fetch', fetch);\n *\n * hooks.extend('fetch', 'before', async (ctx) => {\n * console.log('Fetching:', ctx.args[0]);\n * });\n *\n * await hookedFetch('/api/data');\n */\n make<K extends FunctionProps<Shape>>(\n name: K,\n cb: FuncOrNever<Shape[K]>,\n opts: MakeHookOptions = {}\n ) {\n\n assert(typeof name === 'string', '\"name\" must be a string');\n assert(!this.#registered.has(name), `'${name.toString()}' hook is already registered`);\n assert(isFunction(cb), '\"cb\" must be a function');\n assert(isObject(opts), '\"opts\" must be an object');\n\n this.#registered.add(name);\n\n if (this.#wrapped.has(cb)) {\n\n return this.#wrapped.get(cb) as FuncOrNever<Shape[K]>;\n }\n\n const callback = async (...origArgs: Parameters<FuncOrNever<Shape[K]>>) => {\n\n let returnEarly = false;\n\n const hook = this.#hooks.get(name)!;\n\n const context: HookContext<FuncOrNever<Shape[K]>> = {\n args: origArgs,\n point: 'before',\n removeHook() {},\n returnEarly() {\n returnEarly = true;\n },\n setArgs(next) {\n\n assert(\n Array.isArray(next),\n `setArgs: next args for '${context.point}' '${name.toString()}' must be an array of arguments`\n );\n\n context.args = next;\n },\n setResult(next) {\n context.results = next;\n },\n fail(reason) {\n\n const error = new HookError(`Hook Aborted: ${reason ?? 'unknown'}`);\n\n if (reason instanceof Error) {\n\n error.originalError = reason;\n }\n\n error.extPoint = context.point;\n error.hookName = name as string;\n\n throw error;\n },\n }\n\n const { before, after, error: errorFns } = hook ?? new Hook<FuncOrNever<Shape[K]>>();\n\n const handleSet = async (\n which: typeof before,\n point: keyof typeof hook\n ) => {\n\n context.point = point;\n\n for (const fn of which) {\n\n context.removeHook = () => which.delete(fn);\n\n const opts: HookExtOptions<FuncOrNever<Shape[K]>> = this.#hookFnOpts.get(fn);\n const [, err] = await attempt(() => fn({ ...context }));\n\n if (opts.once) context.removeHook();\n\n if (err && opts.ignoreOnFail !== true) {\n throw err;\n }\n\n if (returnEarly) break;\n }\n }\n\n await handleSet(before, 'before');\n\n if (returnEarly) return context.results!\n\n const [res, err] = await attempt(() => cb.apply(opts?.bindTo || cb, context.args));\n\n context.results = res;\n context.error = err;\n\n if (err) {\n context.point = 'error';\n\n await handleSet(errorFns, 'error');\n\n throw err;\n }\n\n await handleSet(after, 'after');\n\n return context.results!;\n }\n\n return callback as FuncOrNever<Shape[K]>;\n }\n\n /**\n * Wrap an object method in-place to make it hookable.\n *\n * This is a convenience method that combines `make()` with automatic\n * binding and reassignment. The method is replaced on the instance\n * with the wrapped version.\n *\n * @param instance - Object containing the method to wrap\n * @param name - Name of the method to wrap\n * @param opts - Additional options\n *\n * @example\n * class UserService {\n * async save(user: User) { ... }\n * }\n *\n * const service = new UserService();\n * const hooks = new HookEngine<UserService>();\n *\n * hooks.wrap(service, 'save');\n *\n * // Now service.save() is hookable\n * hooks.extend('save', 'before', async (ctx) => {\n * console.log('Saving user:', ctx.args[0]);\n * });\n */\n wrap<K extends FunctionProps<Shape>>(\n instance: Shape,\n name: K,\n opts?: MakeHookOptions\n ) {\n\n assert(isObject(instance), '\"instance\" must be an object');\n\n const wrapped = this.make(\n name,\n instance[name] as FuncOrNever<Shape[K]>,\n {\n bindTo: instance,\n ...opts\n }\n );\n\n this.#wrapped.set(wrapped, instance[name] as AsyncFunc);\n\n instance[name] = wrapped as Shape[K];\n\n }\n\n /**\n * Clear all registered hooks and extensions.\n *\n * After calling this method, all hooks are unregistered and all\n * extensions are removed. Previously wrapped functions will continue\n * to work but without any extensions.\n *\n * @example\n * hooks.wrap(app, 'save');\n * hooks.extend('save', 'before', validator);\n *\n * // Reset for testing\n * hooks.clear();\n *\n * // app.save() still works, but validator no longer runs\n */\n clear() {\n\n this.#registered.clear();\n this.#hooks.clear();\n this.#hookFnOpts = new WeakMap();\n }\n}"],"names":["AssertError","assert","test","message","ErrorClass","isFunction","a","isObject","attempt","fn","e","HookError","__publicField","isHookError","error","Hook","allowedExtPoints","HookEngine","__privateAdd","_registered","_hooks","_hookFnOpts","_wrapped","name","extensionPoint","cbOrOpts","callback","opts","__privateGet","hook","cb","origArgs","returnEarly","context","next","reason","before","after","errorFns","handleSet","which","point","err","res","instance","wrapped","__privateSet"],"mappings":"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"}