@sparkleideas/plugins 3.0.0-alpha.10
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/README.md +401 -0
- package/__tests__/collection-manager.test.ts +332 -0
- package/__tests__/dependency-graph.test.ts +434 -0
- package/__tests__/enhanced-plugin-registry.test.ts +488 -0
- package/__tests__/plugin-registry.test.ts +368 -0
- package/__tests__/ruvector-bridge.test.ts +2429 -0
- package/__tests__/ruvector-integration.test.ts +1602 -0
- package/__tests__/ruvector-migrations.test.ts +1099 -0
- package/__tests__/ruvector-quantization.test.ts +846 -0
- package/__tests__/ruvector-streaming.test.ts +1088 -0
- package/__tests__/sdk.test.ts +325 -0
- package/__tests__/security.test.ts +348 -0
- package/__tests__/utils/ruvector-test-utils.ts +860 -0
- package/examples/plugin-creator/index.ts +636 -0
- package/examples/plugin-creator/plugin-creator.test.ts +312 -0
- package/examples/ruvector/README.md +288 -0
- package/examples/ruvector/attention-patterns.ts +394 -0
- package/examples/ruvector/basic-usage.ts +288 -0
- package/examples/ruvector/docker-compose.yml +75 -0
- package/examples/ruvector/gnn-analysis.ts +501 -0
- package/examples/ruvector/hyperbolic-hierarchies.ts +557 -0
- package/examples/ruvector/init-db.sql +119 -0
- package/examples/ruvector/quantization.ts +680 -0
- package/examples/ruvector/self-learning.ts +447 -0
- package/examples/ruvector/semantic-search.ts +576 -0
- package/examples/ruvector/streaming-large-data.ts +507 -0
- package/examples/ruvector/transactions.ts +594 -0
- package/examples/ruvector-plugins/hook-pattern-library.ts +486 -0
- package/examples/ruvector-plugins/index.ts +79 -0
- package/examples/ruvector-plugins/intent-router.ts +354 -0
- package/examples/ruvector-plugins/mcp-tool-optimizer.ts +424 -0
- package/examples/ruvector-plugins/reasoning-bank.ts +657 -0
- package/examples/ruvector-plugins/ruvector-plugins.test.ts +518 -0
- package/examples/ruvector-plugins/semantic-code-search.ts +498 -0
- package/examples/ruvector-plugins/shared/index.ts +20 -0
- package/examples/ruvector-plugins/shared/vector-utils.ts +257 -0
- package/examples/ruvector-plugins/sona-learning.ts +445 -0
- package/package.json +97 -0
- package/src/collections/collection-manager.ts +661 -0
- package/src/collections/index.ts +56 -0
- package/src/collections/official/index.ts +1040 -0
- package/src/core/base-plugin.ts +416 -0
- package/src/core/plugin-interface.ts +215 -0
- package/src/hooks/index.ts +685 -0
- package/src/index.ts +378 -0
- package/src/integrations/agentic-flow.ts +743 -0
- package/src/integrations/index.ts +88 -0
- package/src/integrations/ruvector/ARCHITECTURE.md +1245 -0
- package/src/integrations/ruvector/attention-advanced.ts +1040 -0
- package/src/integrations/ruvector/attention-executor.ts +782 -0
- package/src/integrations/ruvector/attention-mechanisms.ts +757 -0
- package/src/integrations/ruvector/attention.ts +1063 -0
- package/src/integrations/ruvector/gnn.ts +3050 -0
- package/src/integrations/ruvector/hyperbolic.ts +1948 -0
- package/src/integrations/ruvector/index.ts +394 -0
- package/src/integrations/ruvector/migrations/001_create_extension.sql +135 -0
- package/src/integrations/ruvector/migrations/002_create_vector_tables.sql +259 -0
- package/src/integrations/ruvector/migrations/003_create_indices.sql +328 -0
- package/src/integrations/ruvector/migrations/004_create_functions.sql +598 -0
- package/src/integrations/ruvector/migrations/005_create_attention_functions.sql +654 -0
- package/src/integrations/ruvector/migrations/006_create_gnn_functions.sql +728 -0
- package/src/integrations/ruvector/migrations/007_create_hyperbolic_functions.sql +762 -0
- package/src/integrations/ruvector/migrations/index.ts +35 -0
- package/src/integrations/ruvector/migrations/migrations.ts +647 -0
- package/src/integrations/ruvector/quantization.ts +2036 -0
- package/src/integrations/ruvector/ruvector-bridge.ts +2000 -0
- package/src/integrations/ruvector/self-learning.ts +2376 -0
- package/src/integrations/ruvector/streaming.ts +1737 -0
- package/src/integrations/ruvector/types.ts +1945 -0
- package/src/providers/index.ts +643 -0
- package/src/registry/dependency-graph.ts +568 -0
- package/src/registry/enhanced-plugin-registry.ts +994 -0
- package/src/registry/plugin-registry.ts +604 -0
- package/src/sdk/index.ts +563 -0
- package/src/security/index.ts +594 -0
- package/src/types/index.ts +446 -0
- package/src/workers/index.ts +700 -0
- package/tmp.json +0 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +23 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks Integration Module
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive hook capabilities for plugin development.
|
|
5
|
+
* Enables lifecycle event interception, transformation, and monitoring.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import type {
|
|
10
|
+
HookDefinition,
|
|
11
|
+
HookEvent,
|
|
12
|
+
HookPriority,
|
|
13
|
+
HookHandler,
|
|
14
|
+
HookContext,
|
|
15
|
+
HookResult,
|
|
16
|
+
ILogger,
|
|
17
|
+
IEventBus,
|
|
18
|
+
} from '../types/index.js';
|
|
19
|
+
import { HookEvent as HookEventEnum, HookPriority as HookPriorityEnum } from '../types/index.js';
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Hook Registry
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
export interface HookRegistryConfig {
|
|
26
|
+
logger?: ILogger;
|
|
27
|
+
eventBus?: IEventBus;
|
|
28
|
+
maxHooksPerEvent?: number;
|
|
29
|
+
defaultTimeout?: number;
|
|
30
|
+
parallelExecution?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface HookEntry {
|
|
34
|
+
readonly hook: HookDefinition;
|
|
35
|
+
readonly pluginName?: string;
|
|
36
|
+
readonly registeredAt: Date;
|
|
37
|
+
executionCount: number;
|
|
38
|
+
lastExecuted?: Date;
|
|
39
|
+
lastError?: string;
|
|
40
|
+
avgExecutionTime: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface HookRegistryStats {
|
|
44
|
+
totalHooks: number;
|
|
45
|
+
hooksByEvent: Record<string, number>;
|
|
46
|
+
executionCount: number;
|
|
47
|
+
errorCount: number;
|
|
48
|
+
avgExecutionTime: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Central registry for hook management.
|
|
53
|
+
*/
|
|
54
|
+
export class HookRegistry extends EventEmitter {
|
|
55
|
+
private readonly hooks = new Map<HookEvent, HookEntry[]>();
|
|
56
|
+
private readonly config: HookRegistryConfig;
|
|
57
|
+
private stats = { executionCount: 0, errorCount: 0, totalExecutionTime: 0 };
|
|
58
|
+
|
|
59
|
+
constructor(config?: HookRegistryConfig) {
|
|
60
|
+
super();
|
|
61
|
+
this.config = {
|
|
62
|
+
maxHooksPerEvent: 50,
|
|
63
|
+
defaultTimeout: 30000,
|
|
64
|
+
parallelExecution: false,
|
|
65
|
+
...config,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Register a hook.
|
|
71
|
+
*/
|
|
72
|
+
register(hook: HookDefinition, pluginName?: string): () => void {
|
|
73
|
+
const event = hook.event;
|
|
74
|
+
|
|
75
|
+
if (!this.hooks.has(event)) {
|
|
76
|
+
this.hooks.set(event, []);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const entries = this.hooks.get(event)!;
|
|
80
|
+
|
|
81
|
+
if (entries.length >= (this.config.maxHooksPerEvent ?? 50)) {
|
|
82
|
+
throw new Error(`Maximum hooks limit reached for event ${event}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const entry: HookEntry = {
|
|
86
|
+
hook,
|
|
87
|
+
pluginName,
|
|
88
|
+
registeredAt: new Date(),
|
|
89
|
+
executionCount: 0,
|
|
90
|
+
avgExecutionTime: 0,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Insert in priority order (higher priority first)
|
|
94
|
+
const priority = hook.priority ?? HookPriorityEnum.Normal;
|
|
95
|
+
const insertIndex = entries.findIndex(e => (e.hook.priority ?? HookPriorityEnum.Normal) < priority);
|
|
96
|
+
|
|
97
|
+
if (insertIndex === -1) {
|
|
98
|
+
entries.push(entry);
|
|
99
|
+
} else {
|
|
100
|
+
entries.splice(insertIndex, 0, entry);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Return unregister function
|
|
104
|
+
return () => this.unregister(event, hook.handler);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Unregister a hook.
|
|
109
|
+
*/
|
|
110
|
+
unregister(event: HookEvent, handler: HookHandler): boolean {
|
|
111
|
+
const entries = this.hooks.get(event);
|
|
112
|
+
if (!entries) return false;
|
|
113
|
+
|
|
114
|
+
const index = entries.findIndex(e => e.hook.handler === handler);
|
|
115
|
+
if (index === -1) return false;
|
|
116
|
+
|
|
117
|
+
entries.splice(index, 1);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Execute hooks for an event.
|
|
123
|
+
*/
|
|
124
|
+
async execute(event: HookEvent, data: unknown, source?: string): Promise<HookResult[]> {
|
|
125
|
+
const entries = this.hooks.get(event);
|
|
126
|
+
if (!entries || entries.length === 0) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const context: HookContext = {
|
|
131
|
+
event,
|
|
132
|
+
data,
|
|
133
|
+
timestamp: new Date(),
|
|
134
|
+
source,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const results: HookResult[] = [];
|
|
138
|
+
|
|
139
|
+
if (this.config.parallelExecution) {
|
|
140
|
+
// Execute hooks in parallel (respect priority groups)
|
|
141
|
+
const priorityGroups = this.groupByPriority(entries);
|
|
142
|
+
|
|
143
|
+
for (const group of priorityGroups) {
|
|
144
|
+
const groupResults = await Promise.all(
|
|
145
|
+
group.map(entry => this.executeHook(entry, context))
|
|
146
|
+
);
|
|
147
|
+
results.push(...groupResults);
|
|
148
|
+
|
|
149
|
+
// Check for abort
|
|
150
|
+
if (groupResults.some(r => r.abort)) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// Execute hooks sequentially
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
const result = await this.executeHook(entry, context);
|
|
158
|
+
results.push(result);
|
|
159
|
+
|
|
160
|
+
// Check for abort
|
|
161
|
+
if (result.abort) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Pass modified data to next hook
|
|
166
|
+
if (result.modified && result.data !== undefined) {
|
|
167
|
+
(context as { data: unknown }).data = result.data;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return results;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private groupByPriority(entries: HookEntry[]): HookEntry[][] {
|
|
176
|
+
const groups: HookEntry[][] = [];
|
|
177
|
+
let currentPriority: number | null = null;
|
|
178
|
+
let currentGroup: HookEntry[] = [];
|
|
179
|
+
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const priority = entry.hook.priority ?? HookPriorityEnum.Normal;
|
|
182
|
+
|
|
183
|
+
if (currentPriority === null || currentPriority === priority) {
|
|
184
|
+
currentGroup.push(entry);
|
|
185
|
+
currentPriority = priority;
|
|
186
|
+
} else {
|
|
187
|
+
if (currentGroup.length > 0) {
|
|
188
|
+
groups.push(currentGroup);
|
|
189
|
+
}
|
|
190
|
+
currentGroup = [entry];
|
|
191
|
+
currentPriority = priority;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (currentGroup.length > 0) {
|
|
196
|
+
groups.push(currentGroup);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return groups;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private async executeHook(entry: HookEntry, context: HookContext): Promise<HookResult> {
|
|
203
|
+
const startTime = Date.now();
|
|
204
|
+
this.stats.executionCount++;
|
|
205
|
+
entry.executionCount++;
|
|
206
|
+
entry.lastExecuted = new Date();
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const timeout = this.config.defaultTimeout ?? 30000;
|
|
210
|
+
|
|
211
|
+
const result = await Promise.race([
|
|
212
|
+
entry.hook.handler(context),
|
|
213
|
+
new Promise<never>((_, reject) =>
|
|
214
|
+
setTimeout(() => reject(new Error('Hook execution timeout')), timeout)
|
|
215
|
+
),
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
const duration = Date.now() - startTime;
|
|
219
|
+
this.stats.totalExecutionTime += duration;
|
|
220
|
+
|
|
221
|
+
// Update average execution time
|
|
222
|
+
const totalTime = entry.avgExecutionTime * (entry.executionCount - 1) + duration;
|
|
223
|
+
entry.avgExecutionTime = totalTime / entry.executionCount;
|
|
224
|
+
|
|
225
|
+
return result;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
this.stats.errorCount++;
|
|
228
|
+
entry.lastError = error instanceof Error ? error.message : String(error);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
error: entry.lastError,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get hooks for a specific event.
|
|
239
|
+
*/
|
|
240
|
+
getHooks(event: HookEvent): HookEntry[] {
|
|
241
|
+
return [...(this.hooks.get(event) ?? [])];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get all registered hooks.
|
|
246
|
+
*/
|
|
247
|
+
getAllHooks(): Map<HookEvent, HookEntry[]> {
|
|
248
|
+
return new Map(this.hooks);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get registry statistics.
|
|
253
|
+
*/
|
|
254
|
+
getStats(): HookRegistryStats {
|
|
255
|
+
const hooksByEvent: Record<string, number> = {};
|
|
256
|
+
let totalHooks = 0;
|
|
257
|
+
|
|
258
|
+
for (const [event, entries] of this.hooks) {
|
|
259
|
+
hooksByEvent[event] = entries.length;
|
|
260
|
+
totalHooks += entries.length;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
totalHooks,
|
|
265
|
+
hooksByEvent,
|
|
266
|
+
executionCount: this.stats.executionCount,
|
|
267
|
+
errorCount: this.stats.errorCount,
|
|
268
|
+
avgExecutionTime: this.stats.executionCount > 0
|
|
269
|
+
? this.stats.totalExecutionTime / this.stats.executionCount
|
|
270
|
+
: 0,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Clear all hooks.
|
|
276
|
+
*/
|
|
277
|
+
clear(): void {
|
|
278
|
+
this.hooks.clear();
|
|
279
|
+
this.stats = { executionCount: 0, errorCount: 0, totalExecutionTime: 0 };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// Hook Builder
|
|
285
|
+
// ============================================================================
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Fluent builder for creating hooks.
|
|
289
|
+
*/
|
|
290
|
+
export class HookBuilder {
|
|
291
|
+
private event: HookEvent;
|
|
292
|
+
private name?: string;
|
|
293
|
+
private description?: string;
|
|
294
|
+
private priority: HookPriority = HookPriorityEnum.Normal;
|
|
295
|
+
private isAsync: boolean = true;
|
|
296
|
+
private handler?: HookHandler;
|
|
297
|
+
private conditions: Array<(context: HookContext) => boolean> = [];
|
|
298
|
+
private transformers: Array<(data: unknown) => unknown> = [];
|
|
299
|
+
|
|
300
|
+
constructor(event: HookEvent) {
|
|
301
|
+
this.event = event;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
withName(name: string): this {
|
|
305
|
+
this.name = name;
|
|
306
|
+
return this;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
withDescription(description: string): this {
|
|
310
|
+
this.description = description;
|
|
311
|
+
return this;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
withPriority(priority: HookPriority): this {
|
|
315
|
+
this.priority = priority;
|
|
316
|
+
return this;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
synchronous(): this {
|
|
320
|
+
this.isAsync = false;
|
|
321
|
+
return this;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Add a condition that must be met for the hook to execute.
|
|
326
|
+
*/
|
|
327
|
+
when(condition: (context: HookContext) => boolean): this {
|
|
328
|
+
this.conditions.push(condition);
|
|
329
|
+
return this;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Add a data transformer that runs before the handler.
|
|
334
|
+
*/
|
|
335
|
+
transform(transformer: (data: unknown) => unknown): this {
|
|
336
|
+
this.transformers.push(transformer);
|
|
337
|
+
return this;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Set the handler function.
|
|
342
|
+
*/
|
|
343
|
+
handle(handler: HookHandler): this {
|
|
344
|
+
this.handler = handler;
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Build the hook definition.
|
|
350
|
+
*/
|
|
351
|
+
build(): HookDefinition {
|
|
352
|
+
if (!this.handler) {
|
|
353
|
+
throw new Error(`Hook for event ${this.event} requires a handler`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const originalHandler = this.handler;
|
|
357
|
+
const conditions = this.conditions;
|
|
358
|
+
const transformers = this.transformers;
|
|
359
|
+
|
|
360
|
+
// Wrap handler with conditions and transformers
|
|
361
|
+
const wrappedHandler: HookHandler = async (context: HookContext) => {
|
|
362
|
+
// Check conditions
|
|
363
|
+
for (const condition of conditions) {
|
|
364
|
+
if (!condition(context)) {
|
|
365
|
+
return { success: true, data: context.data };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Apply transformers
|
|
370
|
+
let data = context.data;
|
|
371
|
+
for (const transformer of transformers) {
|
|
372
|
+
data = transformer(data);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Create modified context
|
|
376
|
+
const modifiedContext: HookContext = { ...context, data };
|
|
377
|
+
|
|
378
|
+
// Execute handler
|
|
379
|
+
return originalHandler(modifiedContext);
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
event: this.event,
|
|
384
|
+
handler: wrappedHandler,
|
|
385
|
+
priority: this.priority,
|
|
386
|
+
name: this.name,
|
|
387
|
+
description: this.description,
|
|
388
|
+
async: this.isAsync,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ============================================================================
|
|
394
|
+
// Pre-built Hook Factories
|
|
395
|
+
// ============================================================================
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Factory for creating common hooks.
|
|
399
|
+
*/
|
|
400
|
+
export class HookFactory {
|
|
401
|
+
/**
|
|
402
|
+
* Create a logging hook for any event.
|
|
403
|
+
*/
|
|
404
|
+
static createLogger(
|
|
405
|
+
event: HookEvent,
|
|
406
|
+
logger: ILogger,
|
|
407
|
+
options?: { name?: string; logLevel?: 'debug' | 'info' | 'warn' }
|
|
408
|
+
): HookDefinition {
|
|
409
|
+
const logLevel = options?.logLevel ?? 'debug';
|
|
410
|
+
|
|
411
|
+
return new HookBuilder(event)
|
|
412
|
+
.withName(options?.name ?? `${event}-logger`)
|
|
413
|
+
.withDescription(`Logs ${event} events`)
|
|
414
|
+
.withPriority(HookPriorityEnum.Deferred)
|
|
415
|
+
.handle(async (context) => {
|
|
416
|
+
logger[logLevel](`Hook event: ${event}`, { data: context.data, source: context.source });
|
|
417
|
+
return { success: true };
|
|
418
|
+
})
|
|
419
|
+
.build();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Create a timing hook that measures execution time.
|
|
424
|
+
*/
|
|
425
|
+
static createTimer(
|
|
426
|
+
event: HookEvent,
|
|
427
|
+
_onComplete: (duration: number, context: HookContext) => void
|
|
428
|
+
): HookDefinition {
|
|
429
|
+
return new HookBuilder(event)
|
|
430
|
+
.withName(`${event}-timer`)
|
|
431
|
+
.withDescription(`Times ${event} events`)
|
|
432
|
+
.withPriority(HookPriorityEnum.Critical)
|
|
433
|
+
.handle(async (context) => {
|
|
434
|
+
const startTime = Date.now();
|
|
435
|
+
|
|
436
|
+
// Store start time in metadata
|
|
437
|
+
const metadata = { ...context.metadata, _startTime: startTime };
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
success: true,
|
|
441
|
+
data: context.data,
|
|
442
|
+
metadata,
|
|
443
|
+
};
|
|
444
|
+
})
|
|
445
|
+
.build();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Create a validation hook.
|
|
450
|
+
*/
|
|
451
|
+
static createValidator<T>(
|
|
452
|
+
event: HookEvent,
|
|
453
|
+
validator: (data: T) => boolean | string,
|
|
454
|
+
options?: { name?: string; abortOnFail?: boolean }
|
|
455
|
+
): HookDefinition {
|
|
456
|
+
return new HookBuilder(event)
|
|
457
|
+
.withName(options?.name ?? `${event}-validator`)
|
|
458
|
+
.withDescription(`Validates ${event} data`)
|
|
459
|
+
.withPriority(HookPriorityEnum.High)
|
|
460
|
+
.handle(async (context) => {
|
|
461
|
+
const result = validator(context.data as T);
|
|
462
|
+
|
|
463
|
+
if (result === true) {
|
|
464
|
+
return { success: true };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const error = typeof result === 'string' ? result : 'Validation failed';
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
success: false,
|
|
471
|
+
error,
|
|
472
|
+
abort: options?.abortOnFail ?? false,
|
|
473
|
+
};
|
|
474
|
+
})
|
|
475
|
+
.build();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Create a rate limiting hook.
|
|
480
|
+
*/
|
|
481
|
+
static createRateLimiter(
|
|
482
|
+
event: HookEvent,
|
|
483
|
+
options: { maxPerMinute: number; name?: string }
|
|
484
|
+
): HookDefinition {
|
|
485
|
+
const windowMs = 60000;
|
|
486
|
+
const timestamps: number[] = [];
|
|
487
|
+
|
|
488
|
+
return new HookBuilder(event)
|
|
489
|
+
.withName(options.name ?? `${event}-rate-limiter`)
|
|
490
|
+
.withDescription(`Rate limits ${event} to ${options.maxPerMinute}/min`)
|
|
491
|
+
.withPriority(HookPriorityEnum.Critical)
|
|
492
|
+
.handle(async () => {
|
|
493
|
+
const now = Date.now();
|
|
494
|
+
|
|
495
|
+
// Clean old timestamps
|
|
496
|
+
while (timestamps.length > 0 && timestamps[0] < now - windowMs) {
|
|
497
|
+
timestamps.shift();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (timestamps.length >= options.maxPerMinute) {
|
|
501
|
+
return {
|
|
502
|
+
success: false,
|
|
503
|
+
error: `Rate limit exceeded: ${options.maxPerMinute}/min`,
|
|
504
|
+
abort: true,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
timestamps.push(now);
|
|
509
|
+
return { success: true };
|
|
510
|
+
})
|
|
511
|
+
.build();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Create a caching hook.
|
|
516
|
+
*/
|
|
517
|
+
static createCache<T>(
|
|
518
|
+
event: HookEvent,
|
|
519
|
+
options: {
|
|
520
|
+
keyExtractor: (data: T) => string;
|
|
521
|
+
ttlMs?: number;
|
|
522
|
+
maxSize?: number;
|
|
523
|
+
name?: string;
|
|
524
|
+
}
|
|
525
|
+
): HookDefinition {
|
|
526
|
+
const cache = new Map<string, { value: unknown; expires: number }>();
|
|
527
|
+
const ttlMs = options.ttlMs ?? 60000;
|
|
528
|
+
const maxSize = options.maxSize ?? 100;
|
|
529
|
+
|
|
530
|
+
return new HookBuilder(event)
|
|
531
|
+
.withName(options.name ?? `${event}-cache`)
|
|
532
|
+
.withDescription(`Caches ${event} results`)
|
|
533
|
+
.withPriority(HookPriorityEnum.High)
|
|
534
|
+
.handle(async (context) => {
|
|
535
|
+
const key = options.keyExtractor(context.data as T);
|
|
536
|
+
const now = Date.now();
|
|
537
|
+
|
|
538
|
+
// Check cache
|
|
539
|
+
const cached = cache.get(key);
|
|
540
|
+
if (cached && cached.expires > now) {
|
|
541
|
+
return {
|
|
542
|
+
success: true,
|
|
543
|
+
data: cached.value,
|
|
544
|
+
modified: true,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Clean expired entries if at max size
|
|
549
|
+
if (cache.size >= maxSize) {
|
|
550
|
+
for (const [k, v] of cache) {
|
|
551
|
+
if (v.expires < now) {
|
|
552
|
+
cache.delete(k);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Store result with TTL
|
|
556
|
+
cache.set(key, { value: context.data, expires: now + ttlMs });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return { success: true };
|
|
560
|
+
})
|
|
561
|
+
.build();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Create a retry hook.
|
|
566
|
+
*/
|
|
567
|
+
static createRetry(
|
|
568
|
+
event: HookEvent,
|
|
569
|
+
options: {
|
|
570
|
+
maxRetries: number;
|
|
571
|
+
delayMs?: number;
|
|
572
|
+
backoffMultiplier?: number;
|
|
573
|
+
name?: string;
|
|
574
|
+
}
|
|
575
|
+
): HookDefinition {
|
|
576
|
+
const retryState = new Map<string, number>();
|
|
577
|
+
|
|
578
|
+
return new HookBuilder(event)
|
|
579
|
+
.withName(options.name ?? `${event}-retry`)
|
|
580
|
+
.withDescription(`Adds retry logic to ${event}`)
|
|
581
|
+
.withPriority(HookPriorityEnum.Normal)
|
|
582
|
+
.handle(async (context) => {
|
|
583
|
+
const key = context.source ?? 'default';
|
|
584
|
+
const retryCount = retryState.get(key) ?? 0;
|
|
585
|
+
|
|
586
|
+
if (retryCount >= options.maxRetries) {
|
|
587
|
+
retryState.delete(key);
|
|
588
|
+
return {
|
|
589
|
+
success: false,
|
|
590
|
+
error: `Max retries (${options.maxRetries}) exceeded`,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
success: true,
|
|
596
|
+
data: {
|
|
597
|
+
...context.data as object,
|
|
598
|
+
_retryCount: retryCount,
|
|
599
|
+
},
|
|
600
|
+
modified: true,
|
|
601
|
+
};
|
|
602
|
+
})
|
|
603
|
+
.build();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ============================================================================
|
|
608
|
+
// Hook Executor
|
|
609
|
+
// ============================================================================
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Utility for executing hooks in different patterns.
|
|
613
|
+
*/
|
|
614
|
+
export class HookExecutor {
|
|
615
|
+
private readonly registry: HookRegistry;
|
|
616
|
+
|
|
617
|
+
constructor(registry: HookRegistry) {
|
|
618
|
+
this.registry = registry;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Execute hooks and collect all results.
|
|
623
|
+
*/
|
|
624
|
+
async executeAll(event: HookEvent, data: unknown, source?: string): Promise<HookResult[]> {
|
|
625
|
+
return this.registry.execute(event, data, source);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Execute hooks and return the first successful result.
|
|
630
|
+
*/
|
|
631
|
+
async executeFirst(event: HookEvent, data: unknown, source?: string): Promise<HookResult | null> {
|
|
632
|
+
const results = await this.registry.execute(event, data, source);
|
|
633
|
+
return results.find(r => r.success) ?? null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Execute hooks and return true if all succeeded.
|
|
638
|
+
*/
|
|
639
|
+
async executeValidate(event: HookEvent, data: unknown, source?: string): Promise<boolean> {
|
|
640
|
+
const results = await this.registry.execute(event, data, source);
|
|
641
|
+
return results.every(r => r.success);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Execute hooks and return the final transformed data.
|
|
646
|
+
*/
|
|
647
|
+
async executeTransform<T>(event: HookEvent, data: T, source?: string): Promise<T> {
|
|
648
|
+
const results = await this.registry.execute(event, data, source);
|
|
649
|
+
|
|
650
|
+
let result = data;
|
|
651
|
+
for (const r of results) {
|
|
652
|
+
if (r.success && r.modified && r.data !== undefined) {
|
|
653
|
+
result = r.data as T;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return result;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Execute hooks until one aborts.
|
|
662
|
+
*/
|
|
663
|
+
async executeUntilAbort(
|
|
664
|
+
event: HookEvent,
|
|
665
|
+
data: unknown,
|
|
666
|
+
source?: string
|
|
667
|
+
): Promise<{ results: HookResult[]; aborted: boolean }> {
|
|
668
|
+
const results = await this.registry.execute(event, data, source);
|
|
669
|
+
const aborted = results.some(r => r.abort);
|
|
670
|
+
return { results, aborted };
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ============================================================================
|
|
675
|
+
// Exports
|
|
676
|
+
// ============================================================================
|
|
677
|
+
|
|
678
|
+
export {
|
|
679
|
+
HookEventEnum as HookEvent,
|
|
680
|
+
HookPriorityEnum as HookPriority,
|
|
681
|
+
type HookDefinition,
|
|
682
|
+
type HookHandler,
|
|
683
|
+
type HookContext,
|
|
684
|
+
type HookResult,
|
|
685
|
+
};
|