Skip to content

Commit 3a53c24

Browse files
Copilothotlong
andcommitted
feat: create @objectql/plugin-optimizations package (Phase 1 core refactoring)
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 75b09cd commit 3a53c24

13 files changed

Lines changed: 1970 additions & 0 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "@objectql/plugin-optimizations",
3+
"version": "4.2.0",
4+
"description": "Performance optimization plugins for ObjectQL - connection pooling, LRU cache, compiled hooks",
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
7+
"sideEffects": false,
8+
"exports": {
9+
".": {
10+
"types": "./dist/index.d.ts",
11+
"default": "./dist/index.js"
12+
}
13+
},
14+
"files": ["dist"],
15+
"scripts": {
16+
"build": "tsc",
17+
"test": "vitest run"
18+
},
19+
"dependencies": {
20+
"@objectql/types": "workspace:*",
21+
"@objectstack/core": "^2.0.6",
22+
"@objectstack/spec": "^2.0.6"
23+
},
24+
"devDependencies": {
25+
"typescript": "^5.3.0"
26+
}
27+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/**
2+
* ObjectQL
3+
* Copyright (c) 2026-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { Logger, ConsoleLogger } from '@objectql/types';
10+
11+
/**
12+
* Hook definition
13+
*/
14+
export interface Hook {
15+
pattern: string;
16+
handler: (context: any) => Promise<void> | void;
17+
packageName?: string;
18+
priority?: number;
19+
}
20+
21+
/**
22+
* Compiled Hook Manager
23+
*
24+
* Improvement: Pre-compiles hook pipelines by event pattern at registration time.
25+
* No runtime pattern matching required.
26+
*
27+
* Expected: 5x faster hook execution, parallel async support
28+
*/
29+
export class CompiledHookManager {
30+
// Direct event -> hooks mapping (no pattern matching at runtime)
31+
private pipelines = new Map<string, Hook[]>();
32+
33+
// Keep track of all registered hooks for management
34+
private allHooks = new Map<string, Hook>();
35+
36+
// Structured logger
37+
private logger: Logger = new ConsoleLogger({ name: '@objectql/hook-manager', level: 'warn' });
38+
39+
/**
40+
* Expand a pattern like "before*" to all matching events
41+
*/
42+
private expandPattern(pattern: string): string[] {
43+
// Common event patterns
44+
const eventTypes = [
45+
'beforeCreate', 'afterCreate',
46+
'beforeUpdate', 'afterUpdate',
47+
'beforeDelete', 'afterDelete',
48+
'beforeFind', 'afterFind',
49+
'beforeCount', 'afterCount'
50+
];
51+
52+
// Handle wildcards
53+
if (pattern === '*') {
54+
return eventTypes;
55+
}
56+
57+
if (pattern.includes('*')) {
58+
// Use global replace to handle all occurrences of *
59+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
60+
return eventTypes.filter(event => regex.test(event));
61+
}
62+
63+
// Exact match
64+
return [pattern];
65+
}
66+
67+
/**
68+
* Register a hook - pre-groups by event pattern
69+
*/
70+
registerHook(event: string, objectName: string, handler: any, packageName?: string): void {
71+
const hook: Hook = {
72+
pattern: `${event}:${objectName}`,
73+
handler,
74+
packageName,
75+
priority: 0
76+
};
77+
78+
// Store in all hooks registry
79+
const hookId = `${event}:${objectName}:${Date.now()}`;
80+
this.allHooks.set(hookId, hook);
81+
82+
// Expand event patterns
83+
const events = this.expandPattern(event);
84+
85+
// Handle wildcard object names
86+
if (objectName === '*') {
87+
for (const concreteEvent of events) {
88+
// Register for all potential object names
89+
// Since we don't know all object names upfront, we keep a special '*' pipeline
90+
const wildcardKey = `${concreteEvent}:*`;
91+
if (!this.pipelines.has(wildcardKey)) {
92+
this.pipelines.set(wildcardKey, []);
93+
}
94+
this.pipelines.get(wildcardKey)!.push(hook);
95+
}
96+
} else {
97+
// Pre-group hooks by concrete event names (only for non-wildcard objects)
98+
for (const concreteEvent of events) {
99+
const key = `${concreteEvent}:${objectName}`;
100+
if (!this.pipelines.has(key)) {
101+
this.pipelines.set(key, []);
102+
}
103+
this.pipelines.get(key)!.push(hook);
104+
}
105+
}
106+
}
107+
108+
/**
109+
* Run hooks for an event - direct lookup, no pattern matching
110+
*/
111+
async runHooks(event: string, objectName: string, context: any): Promise<void> {
112+
const key = `${event}:${objectName}`;
113+
const wildcardKey = `${event}:*`;
114+
115+
// Collect all applicable hooks
116+
const hooks: Hook[] = [];
117+
118+
// Add object-specific hooks
119+
const objectHooks = this.pipelines.get(key);
120+
if (objectHooks) {
121+
hooks.push(...objectHooks);
122+
}
123+
124+
// Add wildcard hooks
125+
const wildcardHooks = this.pipelines.get(wildcardKey);
126+
if (wildcardHooks) {
127+
hooks.push(...wildcardHooks);
128+
}
129+
130+
if (hooks.length === 0) {
131+
return;
132+
}
133+
134+
// Sort by priority (higher priority first)
135+
hooks.sort((a, b) => (b.priority || 0) - (a.priority || 0));
136+
137+
// Execute hooks in parallel for better performance
138+
// Note: If order matters, change to sequential execution
139+
await Promise.all(hooks.map(hook => {
140+
try {
141+
return Promise.resolve(hook.handler(context));
142+
} catch (error) {
143+
this.logger.error(`Hook execution failed for ${event}:${objectName}`, error as Error, {
144+
event,
145+
objectName,
146+
});
147+
return Promise.resolve();
148+
}
149+
}));
150+
}
151+
152+
/**
153+
* Remove all hooks from a package
154+
*/
155+
removePackage(packageName: string): void {
156+
// Remove from all hooks registry
157+
const hooksToRemove: string[] = [];
158+
for (const [hookId, hook] of this.allHooks.entries()) {
159+
if (hook.packageName === packageName) {
160+
hooksToRemove.push(hookId);
161+
}
162+
}
163+
hooksToRemove.forEach(id => this.allHooks.delete(id));
164+
165+
// Remove from pipelines
166+
for (const [key, hooks] of this.pipelines.entries()) {
167+
const filtered = hooks.filter(h => h.packageName !== packageName);
168+
if (filtered.length === 0) {
169+
this.pipelines.delete(key);
170+
} else {
171+
this.pipelines.set(key, filtered);
172+
}
173+
}
174+
}
175+
176+
/**
177+
* Clear all hooks
178+
*/
179+
clear(): void {
180+
this.pipelines.clear();
181+
this.allHooks.clear();
182+
}
183+
184+
/**
185+
* Get statistics about registered hooks
186+
*/
187+
getStats(): { totalHooks: number; totalPipelines: number } {
188+
return {
189+
totalHooks: this.allHooks.size,
190+
totalPipelines: this.pipelines.size
191+
};
192+
}
193+
}

0 commit comments

Comments
 (0)