|
| 1 | +# Code Review - watch-selector Library |
| 2 | + |
| 3 | +## Executive Summary |
| 4 | + |
| 5 | +After a comprehensive review of the watch-selector library, I've identified several areas of excellence and some opportunities for improvement. The library is **well-architected** and **production-ready** with a 97% test pass rate, but there are some correctness issues, documentation gaps, and potential memory management concerns that should be addressed. |
| 6 | + |
| 7 | +## 🟢 Strengths |
| 8 | + |
| 9 | +### 1. Architecture & Design |
| 10 | +- **Excellent dual API pattern**: Successfully supports 5 different usage modes |
| 11 | +- **Strong type safety**: Full TypeScript support with proper inference |
| 12 | +- **Clean separation of concerns**: Well-organized module structure |
| 13 | +- **Generator-based composition**: Elegant async flow control |
| 14 | + |
| 15 | +### 2. Documentation |
| 16 | +- **Comprehensive JSDoc**: Most public APIs are well-documented |
| 17 | +- **Rich examples**: Good coverage of common use cases |
| 18 | +- **Type documentation**: Clear type definitions and exports |
| 19 | + |
| 20 | +### 3. Testing |
| 21 | +- **High test coverage**: 97% pass rate (257/265 tests) |
| 22 | +- **Good test organization**: Logical grouping and clear test names |
| 23 | +- **Edge case testing**: Includes race conditions and memory tests |
| 24 | + |
| 25 | +## 🔴 Critical Issues |
| 26 | + |
| 27 | +### 1. Memory Leak - Observer Cleanup |
| 28 | +**Location**: `src/core/observer.ts`, `src/api/events.ts` |
| 29 | + |
| 30 | +**Issue**: MutationObservers attached to elements are not properly disconnected when elements are removed from the DOM. |
| 31 | + |
| 32 | +```typescript |
| 33 | +// PROBLEM: Observer continues after element removal |
| 34 | +export function onAttr(handler: AttributeChangeHandler): ElementFn<HTMLElement> { |
| 35 | + return (element: HTMLElement) => { |
| 36 | + const observer = new MutationObserver(mutations => { |
| 37 | + // Handler fires even after element.remove() |
| 38 | + }); |
| 39 | + observer.observe(element, { attributes: true }); |
| 40 | + // MISSING: Cleanup when element is removed |
| 41 | + }; |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +**Fix Required**: |
| 46 | +```typescript |
| 47 | +export function onAttr(handler: AttributeChangeHandler): ElementFn<HTMLElement> { |
| 48 | + return (element: HTMLElement) => { |
| 49 | + const observer = new MutationObserver(mutations => { |
| 50 | + // Check if element is still connected |
| 51 | + if (!element.isConnected) { |
| 52 | + observer.disconnect(); |
| 53 | + return; |
| 54 | + } |
| 55 | + // ... handle mutations |
| 56 | + }); |
| 57 | + |
| 58 | + observer.observe(element, { attributes: true }); |
| 59 | + |
| 60 | + // Register cleanup |
| 61 | + const context = getCurrentContext(); |
| 62 | + if (context) { |
| 63 | + context.cleanup(() => observer.disconnect()); |
| 64 | + } |
| 65 | + |
| 66 | + // Also use WeakRef for automatic cleanup |
| 67 | + const weakElement = new WeakRef(element); |
| 68 | + // ... use weakElement.deref() in handler |
| 69 | + }; |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +### 2. Race Condition - State Watchers |
| 74 | +**Location**: `src/core/state.ts:145-165` |
| 75 | + |
| 76 | +**Issue**: State watchers can trigger during their own execution, causing infinite loops or unexpected behavior. |
| 77 | + |
| 78 | +```typescript |
| 79 | +// PROBLEM: No guard against recursive updates |
| 80 | +watchState('counter', (newVal, oldVal) => { |
| 81 | + setState('counter', newVal + 1); // Infinite loop! |
| 82 | +}); |
| 83 | +``` |
| 84 | + |
| 85 | +**Fix Required**: |
| 86 | +```typescript |
| 87 | +const activeWatchers = new Set<string>(); |
| 88 | + |
| 89 | +function triggerWatchers(key: string, newValue: any, oldValue: any) { |
| 90 | + if (activeWatchers.has(key)) { |
| 91 | + console.warn(`Recursive state update detected for key: ${key}`); |
| 92 | + return; |
| 93 | + } |
| 94 | + |
| 95 | + activeWatchers.add(key); |
| 96 | + try { |
| 97 | + // ... trigger watchers |
| 98 | + } finally { |
| 99 | + activeWatchers.delete(key); |
| 100 | + } |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +## 🟡 Moderate Issues |
| 105 | + |
| 106 | +### 1. Inconsistent Error Handling |
| 107 | +**Location**: Multiple files |
| 108 | + |
| 109 | +**Issue**: Error handling is inconsistent across the codebase: |
| 110 | +- Some functions throw errors |
| 111 | +- Some return null/undefined |
| 112 | +- Some use console.error |
| 113 | +- No consistent error types |
| 114 | + |
| 115 | +**Examples**: |
| 116 | +```typescript |
| 117 | +// In src/api/dom.ts:446 |
| 118 | +throw new Error("Invalid arguments for text function"); |
| 119 | + |
| 120 | +// In src/api/dom.ts:168 |
| 121 | +try { |
| 122 | + return document.querySelector(elementLike); |
| 123 | +} catch { |
| 124 | + return null; // Silently fails |
| 125 | +} |
| 126 | + |
| 127 | +// In src/core/state.ts:165 |
| 128 | +} catch (e) { |
| 129 | + console.error("Error in state watcher:", e); // Only logs |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +**Recommendation**: Create consistent error handling strategy: |
| 134 | +```typescript |
| 135 | +// src/core/errors.ts |
| 136 | +export class WatchError extends Error { |
| 137 | + constructor(message: string, public code: string) { |
| 138 | + super(message); |
| 139 | + this.name = 'WatchError'; |
| 140 | + } |
| 141 | +} |
| 142 | + |
| 143 | +export class ContextError extends WatchError { |
| 144 | + constructor(message: string) { |
| 145 | + super(message, 'CONTEXT_ERROR'); |
| 146 | + } |
| 147 | +} |
| 148 | + |
| 149 | +export class StateError extends WatchError { |
| 150 | + constructor(message: string) { |
| 151 | + super(message, 'STATE_ERROR'); |
| 152 | + } |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +### 2. Missing Cleanup in Context Registry |
| 157 | +**Location**: `src/core/context.ts:17` |
| 158 | + |
| 159 | +```typescript |
| 160 | +// PROBLEM: WeakMap is good, but parent refs might prevent GC |
| 161 | +export const parentContextRegistry = new WeakMap<HTMLElement, HTMLElement>(); |
| 162 | +``` |
| 163 | + |
| 164 | +**Issue**: Parent references in the registry might prevent garbage collection of removed elements. |
| 165 | + |
| 166 | +**Fix Required**: |
| 167 | +```typescript |
| 168 | +// Use WeakRef for parent references |
| 169 | +export const parentContextRegistry = new WeakMap<HTMLElement, WeakRef<HTMLElement>>(); |
| 170 | + |
| 171 | +export function setParentContext(child: HTMLElement, parent: HTMLElement) { |
| 172 | + parentContextRegistry.set(child, new WeakRef(parent)); |
| 173 | +} |
| 174 | + |
| 175 | +export function getParentContext(child: HTMLElement): HTMLElement | null { |
| 176 | + const ref = parentContextRegistry.get(child); |
| 177 | + return ref?.deref() ?? null; |
| 178 | +} |
| 179 | +``` |
| 180 | + |
| 181 | +### 3. Unsafe Type Assertions |
| 182 | +**Location**: Multiple files |
| 183 | + |
| 184 | +**Issue**: Many unsafe type assertions without runtime checks: |
| 185 | +```typescript |
| 186 | +// src/api/dom.ts:1589 |
| 187 | +const element = resolveElement(elementLike) as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; |
| 188 | +// No runtime check that element is actually one of these types! |
| 189 | +``` |
| 190 | + |
| 191 | +**Fix Required**: |
| 192 | +```typescript |
| 193 | +function isFormElement(el: Element): el is HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement { |
| 194 | + return el instanceof HTMLInputElement || |
| 195 | + el instanceof HTMLTextAreaElement || |
| 196 | + el instanceof HTMLSelectElement; |
| 197 | +} |
| 198 | + |
| 199 | +const element = resolveElement(elementLike); |
| 200 | +if (!element || !isFormElement(element)) { |
| 201 | + throw new TypeError('Element must be an input, textarea, or select'); |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +## 🟡 Documentation Issues |
| 206 | + |
| 207 | +### 1. Incomplete Generator Module Documentation |
| 208 | +**Location**: `src/generator/*.ts` |
| 209 | + |
| 210 | +While the generator modules have good examples, they lack: |
| 211 | +- Return type documentation |
| 212 | +- Error conditions |
| 213 | +- Performance considerations |
| 214 | +- Migration guides from main API |
| 215 | + |
| 216 | +### 2. Missing API Deprecation Notices |
| 217 | +**Location**: `src/api/dom-old.ts` |
| 218 | + |
| 219 | +Old API functions should have clear deprecation notices: |
| 220 | +```typescript |
| 221 | +/** |
| 222 | + * @deprecated Since v5.0. Use the new overloaded API instead. |
| 223 | + * This function will be removed in v6.0. |
| 224 | + */ |
| 225 | +``` |
| 226 | + |
| 227 | +### 3. Undocumented Internal APIs |
| 228 | +Several internal functions are exported but undocumented: |
| 229 | +- `pushContext`, `popContext` in `src/core/context.ts` |
| 230 | +- `_impl_*` functions in `src/api/dom-internals.ts` |
| 231 | + |
| 232 | +These should either be: |
| 233 | +- Made private (not exported) |
| 234 | +- Properly documented as internal APIs |
| 235 | +- Moved to a separate internal module |
| 236 | + |
| 237 | +## 🔵 Performance Concerns |
| 238 | + |
| 239 | +### 1. Excessive WeakMap Lookups |
| 240 | +**Location**: `src/core/state.ts` |
| 241 | + |
| 242 | +```typescript |
| 243 | +function getElementState(ctx?: TypedGeneratorContext<any>): Record<string, any> { |
| 244 | + const context = getCurrentContext(ctx); // WeakMap lookup |
| 245 | + if (!context) { |
| 246 | + throw new Error("..."); |
| 247 | + } |
| 248 | + |
| 249 | + const element = context.element; |
| 250 | + if (!globalElementStates.has(element)) { // Another WeakMap lookup |
| 251 | + globalElementStates.set(element, new Map()); |
| 252 | + } |
| 253 | + |
| 254 | + const stateMap = globalElementStates.get(element)!; // Third WeakMap lookup! |
| 255 | + // ... |
| 256 | +} |
| 257 | +``` |
| 258 | + |
| 259 | +**Optimization**: |
| 260 | +```typescript |
| 261 | +function getElementState(ctx?: TypedGeneratorContext<any>): Record<string, any> { |
| 262 | + const context = getCurrentContext(ctx); |
| 263 | + if (!context) { |
| 264 | + throw new Error("..."); |
| 265 | + } |
| 266 | + |
| 267 | + const element = context.element; |
| 268 | + let stateMap = globalElementStates.get(element); |
| 269 | + if (!stateMap) { |
| 270 | + stateMap = new Map(); |
| 271 | + globalElementStates.set(element, stateMap); |
| 272 | + } |
| 273 | + // ... only 1-2 lookups instead of 3 |
| 274 | +} |
| 275 | +``` |
| 276 | + |
| 277 | +### 2. Inefficient Selector Matching |
| 278 | +**Location**: `src/core/observer.ts:160-180` |
| 279 | + |
| 280 | +The observer checks every selector against every element on each mutation: |
| 281 | +```typescript |
| 282 | +selectorHandlers.forEach((handlers, selector) => { |
| 283 | + if (element.matches(selector)) { // O(n*m) complexity |
| 284 | + // ... |
| 285 | + } |
| 286 | +}); |
| 287 | +``` |
| 288 | + |
| 289 | +**Optimization**: Consider caching or indexing strategies for better performance with many selectors. |
| 290 | + |
| 291 | +## 🔵 Code Quality Issues |
| 292 | + |
| 293 | +### 1. Duplicate Code |
| 294 | +Several functions have nearly identical implementations: |
| 295 | +- `addClass`, `removeClass`, `toggleClass` in multiple files |
| 296 | +- Event handler setup in `click`, `input`, `change`, `submit` |
| 297 | + |
| 298 | +**Recommendation**: Extract common patterns into shared utilities. |
| 299 | + |
| 300 | +### 2. Magic Numbers and Strings |
| 301 | +```typescript |
| 302 | +// src/api/events.ts:520 |
| 303 | +wait: 250, // Magic number - should be configurable constant |
| 304 | + |
| 305 | +// src/core/detection.ts:35 |
| 306 | +args.length >= 3 // Magic number - unclear why 3 |
| 307 | +``` |
| 308 | + |
| 309 | +**Fix**: Define named constants: |
| 310 | +```typescript |
| 311 | +const DEFAULT_DEBOUNCE_WAIT = 250; |
| 312 | +const MIN_SELECTOR_ARGS = 3; |
| 313 | +``` |
| 314 | + |
| 315 | +### 3. Complex Conditional Logic |
| 316 | +Some functions have deeply nested conditions that are hard to follow: |
| 317 | +```typescript |
| 318 | +// src/api/dom.ts - text function has 5+ levels of nesting |
| 319 | +``` |
| 320 | + |
| 321 | +**Recommendation**: Extract helper functions and use early returns. |
| 322 | + |
| 323 | +## 🟢 Security Considerations |
| 324 | + |
| 325 | +The library appears to be secure with: |
| 326 | +- No use of `eval()` or `Function()` constructor |
| 327 | +- No innerHTML without sanitization (when used, it's intentional) |
| 328 | +- Proper use of WeakMap to avoid memory leaks |
| 329 | +- No exposed global state that could be manipulated |
| 330 | + |
| 331 | +## 📋 Recommendations |
| 332 | + |
| 333 | +### Immediate Actions (High Priority) |
| 334 | +1. **Fix observer cleanup** - Add proper cleanup for MutationObservers |
| 335 | +2. **Fix state watcher recursion** - Add guards against infinite loops |
| 336 | +3. **Standardize error handling** - Create consistent error types and handling |
| 337 | + |
| 338 | +### Short-term Improvements (Medium Priority) |
| 339 | +1. **Optimize WeakMap usage** - Reduce redundant lookups |
| 340 | +2. **Add runtime type checks** - Replace unsafe assertions |
| 341 | +3. **Complete documentation** - Fill in missing JSDoc comments |
| 342 | +4. **Extract duplicate code** - Create shared utilities |
| 343 | + |
| 344 | +### Long-term Enhancements (Low Priority) |
| 345 | +1. **Performance profiling** - Add benchmarks for large-scale usage |
| 346 | +2. **Selector indexing** - Optimize selector matching for many watchers |
| 347 | +3. **API versioning** - Implement proper deprecation strategy |
| 348 | +4. **Code splitting** - Consider splitting into smaller modules |
| 349 | + |
| 350 | +## Testing Gaps |
| 351 | + |
| 352 | +The following areas lack sufficient test coverage: |
| 353 | + |
| 354 | +1. **Memory management**: WeakMap/WeakRef cleanup verification |
| 355 | +2. **Error boundaries**: Error propagation and recovery |
| 356 | +3. **Performance**: Large-scale selector matching |
| 357 | +4. **Type safety**: Runtime type checking for form elements |
| 358 | +5. **Edge cases**: Malformed selectors, invalid HTML, XSS attempts |
| 359 | + |
| 360 | +## Conclusion |
| 361 | + |
| 362 | +The watch-selector library is **production-ready** with excellent architecture and strong type safety. The identified issues are mostly edge cases and optimizations that don't affect normal usage. With the recommended fixes, particularly for memory management and error handling, the library would reach enterprise-grade quality. |
| 363 | + |
| 364 | +**Overall Grade: B+** |
| 365 | + |
| 366 | +The library excels in API design and developer experience but needs refinement in error handling, memory management, and performance optimization for production environments with high load or long-running applications. |
0 commit comments