@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.
@@ -0,0 +1,228 @@
1
+ import { AsyncFunc, FunctionProps } from '@logosdx/utils';
2
+ /**
3
+ * Error thrown when a hook extension calls `fail()` or when hook execution fails.
4
+ *
5
+ * @example
6
+ * engine.extend('save', 'before', async (ctx) => {
7
+ * if (!ctx.args[0].isValid) {
8
+ * ctx.fail('Validation failed');
9
+ * }
10
+ * });
11
+ *
12
+ * const [, err] = await attempt(() => app.save(data));
13
+ * if (isHookError(err)) {
14
+ * console.log(err.hookName); // 'save'
15
+ * console.log(err.extPoint); // 'before'
16
+ * }
17
+ */
18
+ export declare class HookError extends Error {
19
+ /** Name of the hook where the error occurred */
20
+ hookName?: string;
21
+ /** Extension point where the error occurred: 'before', 'after', or 'error' */
22
+ extPoint?: string;
23
+ /** Original error if `fail()` was called with an Error instance */
24
+ originalError?: Error;
25
+ /** Whether the hook was explicitly aborted via `fail()` */
26
+ aborted: boolean;
27
+ constructor(message: string);
28
+ }
29
+ /**
30
+ * Type guard to check if an error is a HookError.
31
+ *
32
+ * @example
33
+ * const [result, err] = await attempt(() => app.save(data));
34
+ * if (isHookError(err)) {
35
+ * console.log(`Hook "${err.hookName}" failed at "${err.extPoint}"`);
36
+ * }
37
+ */
38
+ export declare const isHookError: (error: unknown) => error is HookError;
39
+ interface HookShape<F extends AsyncFunc> {
40
+ args: Parameters<F>;
41
+ results?: Awaited<ReturnType<F>>;
42
+ }
43
+ /**
44
+ * Context object passed to hook extension callbacks.
45
+ * Provides access to arguments, results, and control methods.
46
+ *
47
+ * @example
48
+ * engine.extend('fetch', 'before', async (ctx) => {
49
+ * // Read current arguments
50
+ * const [url, options] = ctx.args;
51
+ *
52
+ * // Modify arguments before the original function runs
53
+ * ctx.setArgs([url, { ...options, cache: 'force-cache' }]);
54
+ *
55
+ * // Or skip the original function entirely
56
+ * if (isCached(url)) {
57
+ * ctx.setResult(getCached(url));
58
+ * ctx.returnEarly();
59
+ * }
60
+ * });
61
+ */
62
+ export interface HookContext<F extends AsyncFunc> extends HookShape<F> {
63
+ /** Current extension point: 'before', 'after', or 'error' */
64
+ point: keyof Hook<F>;
65
+ /** Error from the original function (only set in 'error' extensions) */
66
+ error?: unknown;
67
+ /** Abort hook execution with an error. Throws a HookError. */
68
+ fail: (error?: unknown) => never;
69
+ /** Replace the arguments passed to the original function */
70
+ setArgs: (next: Parameters<F>) => void;
71
+ /** Replace the result returned from the hook chain */
72
+ setResult: (next: Awaited<ReturnType<F>>) => void;
73
+ /** Skip the original function and return early with the current result */
74
+ returnEarly: () => void;
75
+ /** Remove this extension from the hook (useful with `once` behavior) */
76
+ removeHook: () => void;
77
+ }
78
+ export type HookFn<F extends AsyncFunc> = (ctx: HookContext<F>) => Promise<void>;
79
+ declare class Hook<F extends AsyncFunc> {
80
+ before: Set<HookFn<F>>;
81
+ after: Set<HookFn<F>>;
82
+ error: Set<HookFn<F>>;
83
+ }
84
+ type HookExtOptions<F extends AsyncFunc> = {
85
+ callback: HookFn<F>;
86
+ once?: true;
87
+ ignoreOnFail?: true;
88
+ };
89
+ type HookExtOrOptions<F extends AsyncFunc> = HookFn<F> | HookExtOptions<F>;
90
+ type MakeHookOptions = {
91
+ bindTo?: any;
92
+ };
93
+ type FuncOrNever<T> = T extends AsyncFunc ? T : never;
94
+ /**
95
+ * A lightweight, type-safe hook system for extending function behavior.
96
+ *
97
+ * HookEngine allows you to wrap functions and add extensions that run
98
+ * before, after, or on error. Extensions can modify arguments, change
99
+ * results, or abort execution entirely.
100
+ *
101
+ * @example
102
+ * interface MyApp {
103
+ * save(data: Data): Promise<Result>;
104
+ * load(id: string): Promise<Data>;
105
+ * }
106
+ *
107
+ * const app = new MyAppImpl();
108
+ * const hooks = new HookEngine<MyApp>();
109
+ *
110
+ * // Wrap a method to make it hookable
111
+ * hooks.wrap(app, 'save');
112
+ *
113
+ * // Add a validation extension
114
+ * hooks.extend('save', 'before', async (ctx) => {
115
+ * if (!ctx.args[0].isValid) {
116
+ * ctx.fail('Validation failed');
117
+ * }
118
+ * });
119
+ *
120
+ * // Add logging extension
121
+ * hooks.extend('save', 'after', async (ctx) => {
122
+ * console.log('Saved:', ctx.results);
123
+ * });
124
+ *
125
+ * @typeParam Shape - Interface defining the hookable functions
126
+ */
127
+ export declare class HookEngine<Shape> {
128
+ #private;
129
+ /**
130
+ * Add an extension to a registered hook.
131
+ *
132
+ * Extensions run at specific points in the hook lifecycle:
133
+ * - `before`: Runs before the original function. Can modify args or return early.
134
+ * - `after`: Runs after successful execution. Can modify the result.
135
+ * - `error`: Runs when the original function throws. Can handle or transform errors.
136
+ *
137
+ * @param name - Name of the registered hook to extend
138
+ * @param extensionPoint - When to run: 'before', 'after', or 'error'
139
+ * @param cbOrOpts - Extension callback or options object
140
+ * @returns Cleanup function to remove the extension
141
+ *
142
+ * @example
143
+ * // Simple callback
144
+ * const cleanup = hooks.extend('save', 'before', async (ctx) => {
145
+ * console.log('About to save:', ctx.args);
146
+ * });
147
+ *
148
+ * // With options
149
+ * hooks.extend('save', 'after', {
150
+ * callback: async (ctx) => { console.log('Saved!'); },
151
+ * once: true, // Remove after first run
152
+ * ignoreOnFail: true // Don't throw if this extension fails
153
+ * });
154
+ *
155
+ * // Later: remove the extension
156
+ * cleanup();
157
+ */
158
+ extend<K extends FunctionProps<Shape>>(name: K, extensionPoint: keyof Hook<FuncOrNever<Shape[K]>>, cbOrOpts: HookExtOrOptions<FuncOrNever<Shape[K]>>): () => void;
159
+ /**
160
+ * Register a function as a hookable and return the wrapped version.
161
+ *
162
+ * The wrapped function behaves identically to the original but allows
163
+ * extensions to be added via `extend()`. Use `wrap()` for a simpler API
164
+ * when working with object methods.
165
+ *
166
+ * @param name - Unique name for this hook (must match a key in Shape)
167
+ * @param cb - The original function to wrap
168
+ * @param opts - Options for the wrapped function
169
+ * @returns Wrapped function with hook support
170
+ *
171
+ * @example
172
+ * const hooks = new HookEngine<{ fetch: typeof fetch }>();
173
+ *
174
+ * const hookedFetch = hooks.make('fetch', fetch);
175
+ *
176
+ * hooks.extend('fetch', 'before', async (ctx) => {
177
+ * console.log('Fetching:', ctx.args[0]);
178
+ * });
179
+ *
180
+ * await hookedFetch('/api/data');
181
+ */
182
+ make<K extends FunctionProps<Shape>>(name: K, cb: FuncOrNever<Shape[K]>, opts?: MakeHookOptions): FuncOrNever<Shape[K]>;
183
+ /**
184
+ * Wrap an object method in-place to make it hookable.
185
+ *
186
+ * This is a convenience method that combines `make()` with automatic
187
+ * binding and reassignment. The method is replaced on the instance
188
+ * with the wrapped version.
189
+ *
190
+ * @param instance - Object containing the method to wrap
191
+ * @param name - Name of the method to wrap
192
+ * @param opts - Additional options
193
+ *
194
+ * @example
195
+ * class UserService {
196
+ * async save(user: User) { ... }
197
+ * }
198
+ *
199
+ * const service = new UserService();
200
+ * const hooks = new HookEngine<UserService>();
201
+ *
202
+ * hooks.wrap(service, 'save');
203
+ *
204
+ * // Now service.save() is hookable
205
+ * hooks.extend('save', 'before', async (ctx) => {
206
+ * console.log('Saving user:', ctx.args[0]);
207
+ * });
208
+ */
209
+ wrap<K extends FunctionProps<Shape>>(instance: Shape, name: K, opts?: MakeHookOptions): void;
210
+ /**
211
+ * Clear all registered hooks and extensions.
212
+ *
213
+ * After calling this method, all hooks are unregistered and all
214
+ * extensions are removed. Previously wrapped functions will continue
215
+ * to work but without any extensions.
216
+ *
217
+ * @example
218
+ * hooks.wrap(app, 'save');
219
+ * hooks.extend('save', 'before', validator);
220
+ *
221
+ * // Reset for testing
222
+ * hooks.clear();
223
+ *
224
+ * // app.save() still works, but validator no longer runs
225
+ */
226
+ clear(): void;
227
+ }
228
+ export {};
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "license": "BSD-3-Clause",
3
+ "homepage": "https://logosdx.dev/",
4
+ "bugs": {
5
+ "url": "https://github.com/logosdx/monorepo/issues",
6
+ "email": "danilo@alonso.network"
7
+ },
8
+ "author": "Danilo Alonso <danilo@alonso.network>",
9
+ "name": "@logosdx/hooks",
10
+ "description": "A lightweight, type-safe hook system for extending function behavior",
11
+ "version": "1.0.0-beta.0",
12
+ "dependencies": {
13
+ "@logosdx/utils": "^5.0.0"
14
+ },
15
+ "keywords": [
16
+ "hooks",
17
+ "middleware",
18
+ "extensions",
19
+ "before after"
20
+ ],
21
+ "files": [
22
+ "dist",
23
+ "src",
24
+ "readme.md",
25
+ "CHANGELOG.md",
26
+ "LICENSE"
27
+ ],
28
+ "exports": {
29
+ ".": {
30
+ "require": "./dist/cjs/index.js",
31
+ "import": "./dist/esm/index.mjs",
32
+ "types": "./dist/types/index.d.ts",
33
+ "browser": "./dist/browser/bundle.js"
34
+ }
35
+ }
36
+ }
package/readme.md ADDED
@@ -0,0 +1,88 @@
1
+ <p align="center">
2
+ <a href="https://logosdx.dev">
3
+ <img src="./docs/public/images/logo.png" alt="LogosDX"/>
4
+ </a>
5
+ </p>
6
+
7
+ <h1 align="center">Logos DX</h1>
8
+
9
+ <p align="center">
10
+ Focused TypeScript utilities for building cross-runtime applications, ETL pipelines, UIs, and more. Use it in browsers, React Native, Node.js, or anywhere JavaScript runs.
11
+ <br/>
12
+ <br/>
13
+ <a href="https://logosdx.dev/">Documentation</a> |
14
+ <a href="https://logosdx.dev/getting-started.html">Getting Started</a> |
15
+ <a href="https://logosdx.dev/cheat-sheet.html">Cheat Sheet</a>
16
+ </p>
17
+
18
+ ---
19
+
20
+ **Logos** */lōgōs/ n.*<br/>
21
+ &nbsp;&nbsp;&nbsp;&nbsp;**¹** From the ancient Greek meaning "divine reason" and "rational principle."<br/>
22
+ &nbsp;&nbsp;&nbsp;&nbsp;**²** Represents the fundamental order that governs the universe.<br/>
23
+ &nbsp;&nbsp;&nbsp;&nbsp;**³** The stoics believed it was the rational law underlying all things.<br/>
24
+
25
+ **DX** */di-eks/ n.*<br/>
26
+ &nbsp;&nbsp;&nbsp;&nbsp;**¹** Stands for "developer experience."<br/>
27
+
28
+ **LogosDX** */lōgōs di-eks/ n.*<br/>
29
+ &nbsp;&nbsp;&nbsp;&nbsp;**¹** A rational developer experience.<br/>
30
+
31
+ ---
32
+
33
+ ## Ready-to-use Packages
34
+
35
+ - `@logosdx/utils`: Production utilities that compose. Resilience built in.
36
+ - `@logosdx/observer`: Events that understand patterns. Queues that manage themselves.
37
+ - `@logosdx/fetch`: HTTP that handles failure. Automatically.
38
+ - `@logosdx/storage`: One API for your many key-value stores.
39
+ - `@logosdx/localize`: Localization utilities for everything from languages to customer-specific strings.
40
+ - `@logosdx/dom`: For those who like to raw-dawg the DOM.
41
+
42
+ ## Under-construction
43
+
44
+ - `@logosdx/state-machine`: State management as streams, not stores.
45
+ - `@logosdx/kit`: Bootstrap your app. Type-safe from day one. All the packages in one place.
46
+
47
+ ## Roadmap
48
+
49
+ - `@logosdx/react`: All of the above, but for React. Use it in Next.js, React Native, or anywhere else.
50
+
51
+ ## LLM Helpers
52
+
53
+ > [!TIP]
54
+ > Don't let AI do your work for you. It's not a replacement for human intelligence. It's a tool to help you.
55
+
56
+ We have LLM helpers available for you to use in Cursor, VSCode, and Claude Code.
57
+
58
+ For more information, see the [LLM Helpers](./llm-helpers/README.md) directory.
59
+
60
+ **Add them to your `.cursor/rules` or `.claude` directory.**
61
+
62
+ ```bash
63
+ # For Claude
64
+ curl -L "https://codeload.github.com/logosdx/monorepo/tar.gz/refs/heads/master" \
65
+ | tar -xz -C .claude --strip-components=2 "monorepo-master/llm-helpers/*.md"
66
+
67
+ # For Cursor
68
+ curl -L "https://codeload.github.com/logosdx/monorepo/tar.gz/refs/heads/master" \
69
+ | tar -xz -C .cursor/rules --strip-components=2 "monorepo-master/llm-helpers/*.md"
70
+ ```
71
+
72
+ ## Philosophy
73
+
74
+ - TypeScript-first
75
+ - Resilience built in
76
+ - Tree-shakable
77
+ - Runtime agnostic
78
+ - Small and fast
79
+ - Debuggable, testable, and well-documented
80
+ - Zero external dependencies
81
+
82
+ ## Contributing
83
+
84
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for development workflow and release process.
85
+
86
+ ## License
87
+
88
+ MIT © [LogosDX](https://logosdx.dev)