SOLUTION: Challenge 3 - Decorator Factory (Call Limit) ====================================================== Challenge: Create a decorator factory that limits method calls to N times. Apply it to a method and verify it stops after the limit. --- SOLUTION: function CallLimit(maxCalls: number) { return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; let callCount = 0; descriptor.value = function(...args: any[]) { if (callCount >= maxCalls) { throw new Error(`${propertyKey} has been called ${maxCalls} times already. No more calls allowed.`); } callCount++; console.log(`[${propertyKey}] Call ${callCount}/${maxCalls}`); return originalMethod.apply(this, args); }; return descriptor; }; } // Test it class APIClient { @CallLimit(3) fetchData() { console.log(" → Fetching data..."); return { data: "response" }; } } const client = new APIClient(); // Calls 1-3 succeed client.fetchData(); // [fetchData] Call 1/3 → Fetching data... client.fetchData(); // [fetchData] Call 2/3 → Fetching data... client.fetchData(); // [fetchData] Call 3/3 → Fetching data... // Call 4 fails try { client.fetchData(); } catch (error) { console.error(error.message); // "fetchData has been called 3 times already. No more calls allowed." } --- EXPLANATION: DECORATOR FACTORY PATTERN: 1. The outer function (CallLimit) accepts the configuration (maxCalls). It returns the actual decorator function. 2. The inner decorator function has access to the maxCalls parameter via closure — it "remembers" it. 3. The wrapper function increments callCount each time the method is called. 4. When the limit is reached, throw an error. STEP-BY-STEP: class APIClient { @CallLimit(3) // <- This calls CallLimit(3), which returns a function fetchData() { } } // Desugared: class APIClient { fetchData() { } } const descriptor = Object.getOwnPropertyDescriptor(APIClient.prototype, "fetchData"); Object.defineProperty(APIClient.prototype, "fetchData", CallLimit(3)(APIClient.prototype, "fetchData", descriptor)); // The @CallLimit(3) is equivalent to: // CallLimit(3) = decorator function // decorator function receives (target, propertyKey, descriptor) // and returns the modified descriptor --- ADVANCED: Reset the limit per instance The above example shares call count across all instances. If you want per-instance limits, store the count on the instance: function CallLimitPerInstance(maxCalls: number) { return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; const callCountKey = `__${propertyKey}_calls__`; descriptor.value = function(...args: any[]) { if (!this[callCountKey]) { this[callCountKey] = 0; } if (this[callCountKey] >= maxCalls) { throw new Error(`${propertyKey} limit reached for this instance.`); } this[callCountKey]++; console.log(`[${propertyKey}] Call ${this[callCountKey]}/${maxCalls}`); return originalMethod.apply(this, args); }; return descriptor; }; } // Now each instance has its own counter: const client1 = new APIClient(); const client2 = new APIClient(); client1.fetchData(); // client1 call 1 client2.fetchData(); // client2 call 1 (separate counter) client1.fetchData(); // client1 call 2 --- PRACTICAL EXAMPLES: // Rate limiting @CallLimit(10) async callAPI() { } // Debug mode: limit noisy methods @CallLimit(5) log() { } // Trial features: free tier gets 3 calls @CallLimit(3) exportToPDF() { } --- WHY DECORATOR FACTORIES MATTER: 1. Parameterization: Same decorator, different configurations. 2. Reusability: Build a library of configurable decorators. 3. Composition: Stack multiple factories on the same method. 4. Framework patterns: Most enterprise frameworks use factories heavily. --- KEY TAKEAWAY: A decorator factory is just a function that returns a decorator. The factory captures the configuration in its closure. This is a core pattern for building reusable, composable decorators.