diff --git a/01-deep-clone/index.js b/01-deep-clone/index.js index e0281df..9c5bd1e 100644 --- a/01-deep-clone/index.js +++ b/01-deep-clone/index.js @@ -9,33 +9,83 @@ * @returns {*} A deep clone of the input value */ function deepClone(value, visited = new WeakMap()) { - // TODO: Implement deep cloning - // Step 1: Handle primitives (return as-is) // Primitives: null, undefined, number, string, boolean, symbol, bigint + if (value == null || typeof value !== 'object') + return value; // Step 2: Check for circular references using the visited WeakMap // If we've seen this object before, return the cached clone + if (visited.has(value)) + return visited.get(value); // Step 3: Handle Date objects // Create a new Date with the same time value + if (value instanceof Date) { + const clonedDate = new Date(value.getTime()); + visited.set(value, clonedDate); + + return clonedDate; + } // Step 4: Handle RegExp objects // Create a new RegExp with the same source and flags + if (value instanceof RegExp) { + const clonedRegExp = new RegExp(value.source, value.flags); + visited.set(value, clonedRegExp); + + return clonedRegExp; + } // Step 5: Handle Map objects // Create a new Map and deep clone each key-value pair + if (value instanceof Map) { + const clonedMap = new Map(); + visited.set(value, clonedMap); + + for (const [key, val] of value) { + clonedMap.set(deepClone(key, visited), deepClone(val, visited)); + } + + return clonedMap; + } // Step 6: Handle Set objects // Create a new Set and deep clone each value + if (value instanceof Set) { + const clonedSet = new Set(); + visited.set(value, clonedSet); + + for (const item of value) { + clonedSet.add(deepClone(item, visited)); + } + + return clonedSet; + } // Step 7: Handle Arrays // Create a new array and deep clone each element + if (Array.isArray(value)) { + const clonedArray = []; + visited.set(value, clonedArray); + + for (let i = 0; i < value.length; i++) { + clonedArray[i] = deepClone(value[i], visited); + } + + return clonedArray; + } // Step 8: Handle plain Objects // Create a new object and deep clone each property + const clonedObject = {}; + visited.set(value, clonedObject); + + for (const key in value) { + clonedObject[key] = deepClone(value[key], visited); + } - return undefined; // Broken: Replace with your implementation + return clonedObject; } module.exports = { deepClone }; diff --git a/02-debounce-throttle/index.js b/02-debounce-throttle/index.js index 397281b..8be5e80 100644 --- a/02-debounce-throttle/index.js +++ b/02-debounce-throttle/index.js @@ -9,21 +9,39 @@ * @returns {Function} The debounced function with a cancel() method */ function debounce(fn, delay) { - // TODO: Implement debounce + // Parameter validation + if (typeof fn !== 'function') { + throw new TypeError(`debounce: invalid type 'fn' argument: ${typeof fn}`); + } + + if (typeof delay !== 'number' || isNaN(delay)) { + throw new TypeError(`debounce: invalid type 'delay' argument: ${typeof fn}`); + } + + // Ensure delay is non-negative + delay = Math.max(0, delay); // Step 1: Create a variable to store the timeout ID + let timerId = null; // Step 2: Create the debounced function that: // - Clears any existing timeout // - Sets a new timeout to call fn after delay // - Preserves `this` context and arguments + function debounced(...args) { + clearTimeout(timerId); + + timerId = setTimeout(() => fn.apply(this, args), delay); + } // Step 3: Add a cancel() method to clear pending timeout + debounced.cancel = () => { + clearTimeout(timerId); + timerId = null; + }; // Step 4: Return the debounced function - - // Return a placeholder that doesn't work - throw new Error("Not implemented"); + return debounced; } /** @@ -37,23 +55,47 @@ function debounce(fn, delay) { * @returns {Function} The throttled function with a cancel() method */ function throttle(fn, limit) { - // TODO: Implement throttle + // Parameter validation + if (typeof fn !== 'function') { + throw new TypeError(`throttle: invalid type 'fn' argument: ${typeof fn}`); + } + + if (typeof limit !== 'number' || isNaN(limit)) { + throw new TypeError(`throttle: invalid type 'limit' argument: ${typeof fn}`); + } + + // Ensure limit is non-negative + limit = Math.max(0, limit); // Step 1: Create variables to track: // - Whether we're currently in a throttle period // - The timeout ID for cleanup + let isThrottled = false; + let timerId = null; // Step 2: Create the throttled function that: // - If not throttling, execute fn immediately and start throttle period // - If throttling, ignore the call // - Preserves `this` context and arguments + function throttled(...args) { + if (!isThrottled) { + fn.apply(this, args); + + isThrottled = true; + + timerId = setTimeout(() => (isThrottled = false), limit); + } + } // Step 3: Add a cancel() method to reset throttle state + throttled.cancel = () => { + isThrottled = false; + clearTimeout(timerId); + timerId = null; + }; // Step 4: Return the throttled function - - // Return a placeholder that doesn't work - throw new Error("Not implemented"); + return throttled; } module.exports = { debounce, throttle }; diff --git a/03-custom-bind/index.js b/03-custom-bind/index.js index 3e691f9..9e91ae6 100644 --- a/03-custom-bind/index.js +++ b/03-custom-bind/index.js @@ -11,29 +11,37 @@ * @returns {Function} A new bound function */ function customBind(fn, context, ...boundArgs) { - // TODO: Implement custom bind - // Step 1: Validate that fn is a function // Throw TypeError if not + if (typeof fn !== 'function') + throw new TypeError(`customBind: invalid type 'fn' argument: ${typeof fn}`); // Step 2: Create the bound function // It should: // - Combine boundArgs with any new arguments // - Call the original function with the combined arguments // - Use the correct `this` context + function bound(...args) { + // Step 3: Handle constructor calls (when used with `new`) + // When called as a constructor: + // - `this` should be a new instance, not the bound context + // - The prototype chain should be preserved + if (new.target) { + const result = fn.apply(this, [...boundArgs, ...args]); + + return result && result instanceof Object ? result : this; + } - // Step 3: Handle constructor calls (when used with `new`) - // When called as a constructor: - // - `this` should be a new instance, not the bound context - // - The prototype chain should be preserved + return fn.apply(context, [...boundArgs, ...args]); + } // Step 4: Preserve the prototype for constructor usage - // boundFunction.prototype = Object.create(fn.prototype) + if (fn.prototype) { + bound.prototype = Object.create(fn.prototype); + } // Step 5: Return the bound function - - // Return placeholder that doesn't work - throw new Error("Not implemented"); + return bound; } /** @@ -44,8 +52,8 @@ function customBind(fn, context, ...boundArgs) { */ // Uncomment and implement: -// Function.prototype.customBind = function(context, ...boundArgs) { -// // Your implementation -// }; +Function.prototype.customBind = function (context, ...boundArgs) { + return customBind(this, context, ...boundArgs); +}; module.exports = { customBind }; diff --git a/04-memoization/index.js b/04-memoization/index.js index dc0f09b..9965530 100644 --- a/04-memoization/index.js +++ b/04-memoization/index.js @@ -11,16 +11,21 @@ * @returns {Function} Memoized function with cache control methods */ function memoize(fn, options = {}) { - // TODO: Implement memoization - // Step 1: Extract options with defaults - // const { maxSize, ttl, keyGenerator } = options; + const { + maxSize = Infinity, + ttl = Infinity, + keyGenerator = defaultKeyGenerator, + } = options; // Step 2: Create the cache (use Map for ordered keys) - // const cache = new Map(); + const cache = new Map(); // Step 3: Create default key generator // Default: JSON.stringify(args) or args.join(',') + function defaultKeyGenerator(args) { + return JSON.stringify(args); + } // Step 4: Create the memoized function // - Generate cache key from arguments @@ -28,29 +33,45 @@ function memoize(fn, options = {}) { // - If cached, return cached value // - If not cached, call fn and store result // - Handle maxSize eviction (remove oldest) + function memoized(...args) { + const key = keyGenerator(args); + const now = Date.now(); + + const cached = cache.get(key); + + if (cached) { + if (now - cached.timestamp < ttl) { + return cached.value; + } + + cache.delete(key); + } + + const value = fn.apply(this, args); + + if (cache.size >= maxSize) { + const oldestKey = cache.keys().next().value; + cache.delete(oldestKey); + } + + cache.set(key, { + value, + timestamp: now, + }); + + return value; + } // Step 5: Add cache control methods - // memoized.cache = { - // clear: () => cache.clear(), - // delete: (key) => cache.delete(key), - // has: (key) => cache.has(key), - // get size() { return cache.size; } - // }; - - // Step 6: Return memoized function - - // Return placeholder that doesn't work - const memoized = function () { - return undefined; - }; memoized.cache = { - clear: () => {}, - delete: () => false, - has: () => false, + clear: () => cache.clear(), + delete: key => cache.delete(key), + has: key => cache.has(key), get size() { - return -1; + return cache.size; }, }; + return memoized; } diff --git a/05-promise-utilities/index.js b/05-promise-utilities/index.js index e59a8eb..0d62ccb 100644 --- a/05-promise-utilities/index.js +++ b/05-promise-utilities/index.js @@ -8,30 +8,38 @@ * @returns {Promise} A promise that resolves to an array of results */ function promiseAll(promises) { - // TODO: Implement promiseAll - // Step 1: Convert iterable to array - // const promiseArray = Array.from(promises); + const promiseArray = Array.from(promises); // Step 2: Handle empty array case // Return Promise.resolve([]) for empty input + if (!promiseArray.length) + return Promise.resolve([]); // Step 3: Create a new Promise - // return new Promise((resolve, reject) => { - - // Step 4: Track results and completion count - // const results = new Array(promiseArray.length); - // let completed = 0; - - // Step 5: Iterate and handle each promise - // - Use Promise.resolve() to handle non-promise values - // - On resolve: store result at correct index, increment count - // - If all completed: resolve with results array - // - On reject: immediately reject the whole promise - - // }); - - return Promise.reject(new Error("Not implemented")); // Broken: Replace with your implementation + return new Promise((resolve, reject) => { + // Step 4: Track results and completion count + const results = new Array(promiseArray.length); + let completed = 0; + + // Step 5: Iterate and handle each promise + // - Use Promise.resolve() to handle non-promise values + // - On resolve: store result at correct index, increment count + // - If all completed: resolve with results array + // - On reject: immediately reject the whole promise + + for (let i = 0; i < results.length; i++) { + Promise.resolve(promiseArray[i]) + .then(value => { + completed++; + results[i] = value; + + if (completed === results.length) + resolve(results); + }) + .catch(error => reject(error)); + } + }); } /** @@ -43,19 +51,23 @@ function promiseAll(promises) { * @returns {Promise} A promise that settles with the first result */ function promiseRace(promises) { - // TODO: Implement promiseRace - // Step 1: Convert iterable to array + const promiseArray = Array.from(promises); // Step 2: Handle empty array (return pending promise) // For empty array, return a promise that never settles + if (!promiseArray.length) + return new Promise(() => {}); // Step 3: Create a new Promise // The first promise to settle wins // Step 4: For each promise, attach then/catch that resolves/rejects the race - - return new Promise(() => {}); // Replace with your implementation + return new Promise((resolve, reject) => { + for (const promise of promiseArray) { + Promise.resolve(promise).then(resolve, reject); + } + }); } /** @@ -68,24 +80,42 @@ function promiseRace(promises) { * @returns {Promise} A promise that resolves to an array of settlement objects */ function promiseAllSettled(promises) { - // TODO: Implement promiseAllSettled - // Step 1: Convert iterable to array + const promiseArray = Array.from(promises); // Step 2: Handle empty array case + if (!promiseArray.length) + return Promise.resolve([]); // Step 3: Create a new Promise - - // Step 4: Track results and completion count - // Each result is: { status: 'fulfilled', value } or { status: 'rejected', reason } - - // Step 5: For each promise: - // - On resolve: store { status: 'fulfilled', value } - // - On reject: store { status: 'rejected', reason } - // - Never reject the outer promise - // - Resolve when all have settled - - return Promise.reject(new Error("Not implemented")); // Broken: Replace with your implementation + return new Promise(resolve => { + // Step 4: Track results and completion count + // Each result is: { status: 'fulfilled', value } or { status: 'rejected', reason } + const results = new Array(promiseArray.length); + let completed = 0; + + // Step 5: For each promise: + // - On resolve: store { status: 'fulfilled', value } + // - On reject: store { status: 'rejected', reason } + // - Never reject the outer promise + // - Resolve when all have settled + for (let i = 0; i < promiseArray.length; i++) { + Promise.resolve(promiseArray[i]).then( + value => { + results[i] = { status: 'fulfilled', value }; + completed++; + + if (completed === results.length) resolve(results); + }, + reason => { + results[i] = { status: 'rejected', reason }; + completed++; + + if (completed === results.length) resolve(results); + } + ); + } + }); } /** @@ -98,27 +128,35 @@ function promiseAllSettled(promises) { * @returns {Promise} A promise that resolves with the first fulfilled value */ function promiseAny(promises) { - // TODO: Implement promiseAny - // Step 1: Convert iterable to array + const promiseArray = Array.from(promises); // Step 2: Handle empty array (reject with AggregateError) + if (!promiseArray.length) + return Promise.reject(new AggregateError([])); // Step 3: Create a new Promise - - // Step 4: Track rejection count and errors - // const errors = []; - // let rejectedCount = 0; - - // Step 5: For each promise: - // - On resolve: immediately resolve the outer promise (first wins) - // - On reject: collect error, increment count - // - If all rejected: reject with AggregateError - - // Note: AggregateError is created like: - // new AggregateError(errorsArray, 'All promises were rejected') - - return Promise.reject(new AggregateError([], "No promises")); // Replace + return new Promise((resolve, reject) => { + // Step 4: Track rejection count and errors + const errors = []; + let rejectedCount = 0; + + // Step 5: For each promise: + // - On resolve: immediately resolve the outer promise (first wins) + // - On reject: collect error, increment count + // - If all rejected: reject with AggregateError + for (const promise of promiseArray) { + Promise.resolve(promise) + .then(resolve) + .catch(error => { + errors.push(error); + rejectedCount++; + + if (rejectedCount === promiseArray.length) + reject(new AggregateError(errors, 'All promises were rejected')); + }); + } + }); } module.exports = { promiseAll, promiseRace, promiseAllSettled, promiseAny }; diff --git a/06-async-queue/index.js b/06-async-queue/index.js index 2ab4d0a..6d67290 100644 --- a/06-async-queue/index.js +++ b/06-async-queue/index.js @@ -11,7 +11,6 @@ class AsyncQueue { * @param {boolean} [options.autoStart=true] - Start processing immediately */ constructor(options = {}) { - // TODO: Initialize the queue // Step 1: Extract options with defaults // this.concurrency = options.concurrency || 1; // this.autoStart = options.autoStart !== false; @@ -20,6 +19,14 @@ class AsyncQueue { // this.running = 0; // Currently running count // this.paused = false; // Paused state // this.emptyCallbacks = []; // Callbacks for empty event + const { concurrency = 1, autoStart = true } = options; + this.concurrency = concurrency; + this.autoStart = autoStart; + + this.queue = []; + this.running = 0; + this.paused = false; + this.emptyCallbacks = []; } /** @@ -30,44 +37,66 @@ class AsyncQueue { * @returns {Promise} Resolves when task completes */ add(task, options = {}) { - // TODO: Implement add + let entryResolve; + let entryReject; // Step 1: Create a new Promise and store its resolve/reject + const promise = new Promise((resolve, reject) => { + entryResolve = resolve; + entryReject = reject; + }); // Step 2: Create task entry with: task, priority, resolve, reject + const entry = { + task, + priority: options.priority ?? 0, + resolve: entryResolve, + reject: entryReject, + }; // Step 3: Add to queue (consider priority ordering) + this.queue.push(entry); + this.queue.sort((a, b) => b.priority - a.priority); // Step 4: Try to process if autoStart and not paused + if (this.autoStart && !this.paused) + this._process(); // Step 5: Return the promise - - return Promise.resolve(); // Replace with your implementation + return promise; } /** * Start processing the queue */ start() { - // TODO: Implement start // Set paused to false and trigger processing + this.paused = false; + this._process(); } /** * Pause the queue (running tasks will complete) */ pause() { - // TODO: Implement pause // Set paused to true + this.paused = true; } /** * Clear all pending tasks */ clear() { - // TODO: Implement clear // Empty the queue array // Optionally: reject pending promises with an error + // for (const entry of this.queue) { + // try{ + // const error = new Error('Rejected during clearing queue'); + // entry.reject(error); + // } catch (e){} + + // } + this.queue.length = 0; } /** @@ -75,8 +104,8 @@ class AsyncQueue { * @param {Function} callback - Called when queue is empty */ onEmpty(callback) { - // TODO: Implement onEmpty // Store callback to be called when size becomes 0 and nothing running + this.emptyCallbacks.push(callback); } /** @@ -84,8 +113,7 @@ class AsyncQueue { * @returns {number} */ get size() { - // TODO: Return queue length - throw new Error("Not implemented"); + return this.queue.length; } /** @@ -93,8 +121,7 @@ class AsyncQueue { * @returns {number} */ get pending() { - // TODO: Return running count - throw new Error("Not implemented"); + return this.running; } /** @@ -102,8 +129,7 @@ class AsyncQueue { * @returns {boolean} */ get isPaused() { - // TODO: Return paused state - throw new Error("Not implemented"); + return this.paused; } /** @@ -111,7 +137,6 @@ class AsyncQueue { * @private */ _process() { - // TODO: Implement _process // Step 1: Check if we can run more tasks // - Not paused // - Running count < concurrency @@ -122,6 +147,23 @@ class AsyncQueue { // - On success: resolve the task's promise // - On error: reject the task's promise // - Always: decrement running, call _process again, check if empty + while (this.running < this.concurrency + && !this.paused + && this.queue.length !== 0) { + const entry = this.queue.shift(); + this.running++; + + Promise.resolve(entry.task()) + .then( + value => entry.resolve(value), + error => entry.reject(error) + ) + .finally(() => { + this.running--; + this._checkEmpty(); + this._process(); + }); + } } /** @@ -129,7 +171,11 @@ class AsyncQueue { * @private */ _checkEmpty() { - // TODO: If queue is empty and nothing running, call empty callbacks + if (this.queue.length === 0 && this.running === 0) { + this.emptyCallbacks.forEach(callback => callback()); + + this.emptyCallbacks.length = 0; + } } } diff --git a/06-async-queue/index.test.js b/06-async-queue/index.test.js index 04f3dc9..b4029c7 100644 --- a/06-async-queue/index.test.js +++ b/06-async-queue/index.test.js @@ -251,6 +251,8 @@ describe("AsyncQueue", () => { await queue.add(async () => "task"); + await delay(10); + expect(emptyCalled).toBe(true); }); @@ -263,6 +265,8 @@ describe("AsyncQueue", () => { await queue.add(async () => {}); + await delay(10); + expect(calls).toEqual(["callback1", "callback2"]); }); diff --git a/07-retry-with-backoff/index.js b/07-retry-with-backoff/index.js index ed7fd75..3d831af 100644 --- a/07-retry-with-backoff/index.js +++ b/07-retry-with-backoff/index.js @@ -15,38 +15,55 @@ * @returns {Promise} Result of fn or throws last error */ async function retry(fn, options = {}) { - // TODO: Implement retry with backoff - // Step 1: Extract options with defaults - // const { - // maxRetries = 3, - // initialDelay = 1000, - // maxDelay = 30000, - // backoff = 'exponential', - // jitter = false, - // retryIf = () => true, - // onRetry = () => {} - // } = options; + const { + maxRetries = 3, + initialDelay = 1000, + maxDelay = 30000, + backoff = 'exponential', + jitter = false, + retryIf = () => true, + onRetry = () => {}, + } = options; // Step 2: Initialize attempt counter and last error + let lastError; // Step 3: Loop up to maxRetries + 1 (initial attempt + retries) + for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { + // Step 4: Try to execute fn + // - On success: return result + // - On error: check if should retry + try { + return await fn(); + } catch (e) { + lastError = e; + // Step 5: If should retry: + // - Call onRetry callback + // - Calculate delay based on backoff strategy + // - Apply maxDelay cap + // - Apply jitter if enabled + // - Wait for delay + // - Continue to next attempt + if (retryIf(e) && attempt <= maxRetries) { + onRetry(e, attempt); - // Step 4: Try to execute fn - // - On success: return result - // - On error: check if should retry + let delay = + Math.min(calculateDelay(backoff, attempt, initialDelay), maxDelay); + delay = jitter ? applyJitter(delay) : delay; - // Step 5: If should retry: - // - Call onRetry callback - // - Calculate delay based on backoff strategy - // - Apply maxDelay cap - // - Apply jitter if enabled - // - Wait for delay - // - Continue to next attempt + await sleep(delay); + } else { + break; + } + } + } // Step 6: If all retries exhausted, throw last error - - throw new Error("Not implemented"); // Replace with your implementation + if (lastError === undefined) { + throw new Error('Retry failed but no error was captured'); + } + throw lastError; } /** @@ -58,13 +75,17 @@ async function retry(fn, options = {}) { * @returns {number} Calculated delay in ms */ function calculateDelay(strategy, attempt, initialDelay) { - // TODO: Implement delay calculation - // Fixed: delay = initialDelay // Linear: delay = initialDelay * attempt // Exponential: delay = initialDelay * 2^(attempt-1) - - throw new Error("Not implemented"); + switch (strategy) { + case 'linear': + return initialDelay * attempt; + case 'exponential': + return initialDelay * Math.pow(2, attempt - 1); + default: + return initialDelay; + } } /** @@ -74,10 +95,7 @@ function calculateDelay(strategy, attempt, initialDelay) { * @returns {number} Delay with random jitter (0-25% added) */ function applyJitter(delay) { - // TODO: Add 0-25% random jitter - // return delay * (1 + Math.random() * 0.25); - - throw new Error("Not implemented"); + return delay * (1 + Math.random() * 0.25); } /** diff --git a/07-retry-with-backoff/index.test.js b/07-retry-with-backoff/index.test.js index a4ca6c8..efd6a4e 100644 --- a/07-retry-with-backoff/index.test.js +++ b/07-retry-with-backoff/index.test.js @@ -1,5 +1,11 @@ const { retry, calculateDelay, applyJitter } = require("./index"); +// helper to prevent unhandled rejection warnings with jest fake timers +function preventUnhandledRejection(promise) { + promise.catch(() => {}); + return promise; +} + describe("retry", () => { beforeEach(() => { jest.useFakeTimers(); @@ -44,7 +50,7 @@ describe("retry", () => { const error = new Error("persistent failure"); const fn = jest.fn().mockRejectedValue(error); - const promise = retry(fn, { maxRetries: 2, initialDelay: 100 }); + const promise = preventUnhandledRejection(retry(fn, { maxRetries: 2, initialDelay: 100 })); await jest.advanceTimersByTimeAsync(0); // attempt 1 await jest.advanceTimersByTimeAsync(100); // attempt 2 @@ -59,7 +65,7 @@ describe("retry", () => { test("should respect maxRetries", async () => { const fn = jest.fn().mockRejectedValue(new Error("fail")); - const promise = retry(fn, { maxRetries: 5, initialDelay: 10 }); + const promise = preventUnhandledRejection(retry(fn, { maxRetries: 5, initialDelay: 10 })); for (let i = 0; i < 10; i++) { await jest.advanceTimersByTimeAsync(100); @@ -72,7 +78,7 @@ describe("retry", () => { test("should handle maxRetries of 0", async () => { const fn = jest.fn().mockRejectedValue(new Error("fail")); - const promise = retry(fn, { maxRetries: 0 }); + const promise = preventUnhandledRejection(retry(fn, { maxRetries: 0 })); await jest.advanceTimersByTimeAsync(0); await expect(promise).rejects.toThrow(); @@ -102,9 +108,9 @@ describe("retry", () => { test("should not retry when retryIf returns false", async () => { const fn = jest.fn().mockRejectedValue({ status: 400 }); - const promise = retry(fn, { + const promise = preventUnhandledRejection(retry(fn, { retryIf: (error) => error.status >= 500, - }); + })); await jest.advanceTimersByTimeAsync(0); await expect(promise).rejects.toEqual({ status: 400 }); @@ -192,12 +198,12 @@ describe("retry with backoff strategies", () => { const delays = []; const fn = jest.fn().mockRejectedValue(new Error("fail")); - const promise = retry(fn, { + const promise = preventUnhandledRejection(retry(fn, { maxRetries: 5, initialDelay: 1000, maxDelay: 5000, backoff: "exponential", - }); + })); // Track delays by advancing time // exponential would be: 1000, 2000, 4000, 8000, 16000 diff --git a/08-event-emitter/index.js b/08-event-emitter/index.js index b7c9a7c..ee8605f 100644 --- a/08-event-emitter/index.js +++ b/08-event-emitter/index.js @@ -5,8 +5,8 @@ */ class EventEmitter { constructor() { - // TODO: Initialize event storage // this.events = new Map(); // or {} + this.events = new Map(); } /** @@ -16,15 +16,15 @@ class EventEmitter { * @returns {EventEmitter} this (for chaining) */ on(event, listener) { - // TODO: Implement on - // Step 1: Get or create the listeners array for this event + if (!this.events.has(event)) + this.events.set(event, []); // Step 2: Add the listener to the array + this.events.get(event).push(listener); // Step 3: Return this for chaining - - return null; // Broken: should return this + return this; } /** @@ -34,16 +34,24 @@ class EventEmitter { * @returns {EventEmitter} this (for chaining) */ off(event, listener) { - // TODO: Implement off - // Step 1: Get the listeners array for this event + const listeners = this.events.get(event); + if (!listeners) + return this; // Step 2: Find and remove the listener // Note: Handle wrapped 'once' listeners + const listenerIdx = + listeners.findIndex(item => item === listener || item._origin === listener); - // Step 3: Return this for chaining + if (listenerIdx !== -1) + listeners.splice(listenerIdx, 1); - return null; // Broken: should return this + if(listeners.length === 0) + this.events.delete(event); + + // Step 3: Return this for chaining + return this; } /** @@ -53,18 +61,25 @@ class EventEmitter { * @returns {boolean} true if event had listeners */ emit(event, ...args) { - // TODO: Implement emit - - // Step 1: Get the listeners array for this event + // Step 1: Get the listeners array for this event (already a copy) + const listeners = this.listeners(event); // Step 2: If no listeners, return false + if (listeners.length === 0) + return false; // Step 3: Call each listener with the arguments - // Make a copy of the array to handle removals during emit + // Note: listeners is already a copy, safe to iterate even if removals occur + listeners.forEach(listener => { + try { + listener.apply(this, args); + } catch (error) { + console.error(`emit: error in listener for event '${event}':`, error); + } + }); // Step 4: Return true - - throw new Error("Not implemented"); + return true; } /** @@ -74,19 +89,22 @@ class EventEmitter { * @returns {EventEmitter} this (for chaining) */ once(event, listener) { - // TODO: Implement once - // Step 1: Create a wrapper function that: // - Removes itself after being called // - Calls the original listener with arguments + function wrapped(...args) { + this.off(event, wrapped); + listener.apply(this, args); + } // Step 2: Store reference to original listener for 'off' to work + wrapped._origin = listener; // Step 3: Register the wrapper with 'on' + this.on(event, wrapped); // Step 4: Return this for chaining - - return null; // Broken: should return this + return this; } /** @@ -95,12 +113,15 @@ class EventEmitter { * @returns {EventEmitter} this (for chaining) */ removeAllListeners(event) { - // TODO: Implement removeAllListeners - // If event is provided, remove only that event's listeners // If no event, clear all events + if (event !== undefined) { + this.events.delete(event); + } else { + this.events.clear(); + } - return null; // Broken: should return this + return this; } /** @@ -109,11 +130,10 @@ class EventEmitter { * @returns {Function[]} Array of listener functions */ listeners(event) { - // TODO: Implement listeners - // Return copy of listeners array, or empty array if none + const eventListeners = this.events.get(event); - throw new Error("Not implemented"); + return eventListeners ? [...eventListeners] : []; } /** @@ -122,9 +142,9 @@ class EventEmitter { * @returns {number} Listener count */ listenerCount(event) { - // TODO: Implement listenerCount + const eventListeners = this.events.get(event); - throw new Error("Not implemented"); + return eventListeners ? eventListeners.length : 0; } } diff --git a/09-observable/index.js b/09-observable/index.js index a7e8f15..83f3417 100644 --- a/09-observable/index.js +++ b/09-observable/index.js @@ -9,8 +9,7 @@ class Observable { * @param {Function} subscribeFn - Function called with subscriber on subscribe */ constructor(subscribeFn) { - // TODO: Store the subscribe function - // this._subscribeFn = subscribeFn; + this._subscribeFn = subscribeFn; } /** @@ -19,23 +18,51 @@ class Observable { * @returns {Object} Subscription with unsubscribe method */ subscribe(observer) { - // TODO: Implement subscribe - // Step 1: Normalize observer (handle function shorthand) // If observer is a function, wrap it: { next: observer } + if (typeof observer === 'function') { + observer = { + next: observer, + }; + } // Step 2: Create a subscriber object that: // - Has next, error, complete methods // - Tracks if completed/errored (stops accepting values) // - Calls observer methods when appropriate + const subscriber = { + completed: false, + errored: false, + next(value) { + if (subscriber.completed || subscriber.errored) return; + observer.next && observer.next(value); + }, + error(err) { + if (subscriber.completed || subscriber.errored) return; + subscriber.errored = true; + observer.error && observer.error(err); + }, + complete() { + if (subscriber.completed || subscriber.errored) return; + subscriber.completed = true; + observer.complete && observer.complete(); + }, + }; // Step 3: Call the subscribe function with the subscriber // Step 4: Handle cleanup function returned by subscribeFn + const cleanup = this._subscribeFn(subscriber); // Step 5: Return subscription object with unsubscribe method - - throw new Error("Not implemented"); + return { + unsubscribe() { + if (cleanup && typeof cleanup === 'function') cleanup(); + + subscriber.completed = true; + subscriber.errored = true; + }, + }; } /** @@ -44,14 +71,25 @@ class Observable { * @returns {Observable} New Observable with transformed values */ map(fn) { - // TODO: Implement map operator - // Return new Observable that: // - Subscribes to source (this) // - Calls fn on each value // - Emits transformed value - - return new Observable(() => {}); // Broken: Replace with implementation + return new Observable(subscriber => { + const subscription = this.subscribe({ + next(value) { + subscriber.next(fn(value)); + }, + error(e) { + subscriber.error(e); + }, + complete() { + subscriber.complete(); + }, + }); + + return () => subscription.unsubscribe(); + }); } /** @@ -60,13 +98,27 @@ class Observable { * @returns {Observable} New Observable with filtered values */ filter(predicate) { - // TODO: Implement filter operator - // Return new Observable that: // - Subscribes to source (this) // - Only emits values where predicate returns true - return new Observable(() => {}); // Broken: Replace with implementation + return new Observable(subscriber => { + const subscription = this.subscribe({ + next(value) { + if (predicate(value)) { + subscriber.next(value); + } + }, + error(e) { + subscriber.error(e); + }, + complete() { + subscriber.complete(); + }, + }); + + return () => subscription.unsubscribe(); + }); } /** @@ -75,14 +127,38 @@ class Observable { * @returns {Observable} New Observable limited to count values */ take(count) { - // TODO: Implement take operator - // Return new Observable that: // - Subscribes to source (this) // - Emits first `count` values // - Completes after `count` values - - return new Observable(() => {}); // Broken: Replace with implementation + return new Observable(subscriber => { + let taken = 0; + let completed = false; + + const subscription = this.subscribe({ + next(value) { + if (taken < count) { + subscriber.next(value); + taken++; + + if (taken === count) { + completed = true; + subscriber.complete(); + } + } + }, + error(e) { + subscriber.error(e); + }, + complete() { + if (!completed) { + subscriber.complete(); + } + }, + }); + + return () => subscription.unsubscribe(); + }); } /** @@ -91,14 +167,33 @@ class Observable { * @returns {Observable} New Observable that skips first count values */ skip(count) { - // TODO: Implement skip operator - // Return new Observable that: // - Subscribes to source (this) // - Ignores first `count` values // - Emits remaining values - return new Observable(() => {}); // Broken: Replace with implementation + return new Observable(subscriber => { + let skipped = 0; + + const subscription = this.subscribe({ + next(value) { + if (skipped < count) { + skipped++; + return; + } + + subscriber.next(value); + }, + error(e) { + subscriber.error(e); + }, + complete() { + subscriber.complete(); + }, + }); + + return () => subscription.unsubscribe(); + }); } /** @@ -107,15 +202,17 @@ class Observable { * @returns {Observable} Observable that emits array values */ static from(array) { - // TODO: Implement from - // Return new Observable that: // - Emits each array element // - Completes after last element - - return new Observable((subscriber) => { + return new Observable(subscriber => { // subscriber.next(...) for each // subscriber.complete() + array.forEach(value => subscriber.next(value)); + + subscriber.complete(); + + return () => {}; }); } @@ -125,8 +222,6 @@ class Observable { * @returns {Observable} Observable that emits single value */ static of(...values) { - // TODO: Implement of - // Return new Observable that emits all values then completes return Observable.from(values); diff --git a/10-lru-cache/index.js b/10-lru-cache/index.js index f087769..9c2a83f 100644 --- a/10-lru-cache/index.js +++ b/10-lru-cache/index.js @@ -9,11 +9,10 @@ class LRUCache { * @param {number} capacity - Maximum number of items */ constructor(capacity) { - // TODO: Initialize the cache // Step 1: Store capacity - // this.capacity = capacity; + this.capacity = capacity; // Step 2: Create storage (Map recommended) - // this.cache = new Map(); + this.cache = new Map(); } /** @@ -22,18 +21,21 @@ class LRUCache { * @returns {*} Value or undefined if not found */ get(key) { - // TODO: Implement get - // Step 1: Check if key exists // Step 2: If exists: // - Get the value // - Move to end (most recent) // - Return value + if (this.cache.has(key)) { + const value = this.cache.get(key); + this.cache.delete(key); + this.cache.set(key, value); + return value; + } // Step 3: If not exists, return undefined - - throw new Error("Not implemented"); + return undefined; } /** @@ -42,10 +44,17 @@ class LRUCache { * @param {*} value - Value to store */ put(key, value) { - // TODO: Implement put // Step 1: If key already exists, delete it first (to update position) // Step 2: If at capacity, evict least recently used (first item) // Step 3: Add the new key-value pair (goes to end = most recent) + if (this.cache.has(key)) { + this.cache.delete(key); + } else if (this.size === this.capacity) { + const firstKey = this.cache.keys().next().value; + this.cache.delete(firstKey); + } + + this.cache.set(key, value); } /** @@ -54,9 +63,7 @@ class LRUCache { * @returns {boolean} */ has(key) { - // TODO: Implement has - - throw new Error("Not implemented"); + return this.cache.has(key); } /** @@ -65,17 +72,14 @@ class LRUCache { * @returns {boolean} true if key existed */ delete(key) { - // TODO: Implement delete - - throw new Error("Not implemented"); + return this.cache.delete(key); } /** * Clear all items */ clear() { - // TODO: Implement clear - throw new Error("Not implemented"); + this.cache.clear(); } /** @@ -83,9 +87,7 @@ class LRUCache { * @returns {number} */ get size() { - // TODO: Return current size - - throw new Error("Not implemented"); + return this.cache.size; } /** @@ -93,9 +95,7 @@ class LRUCache { * @returns {Array} Array of keys */ keys() { - // TODO: Return array of keys - - throw new Error("Not implemented"); + return Array.from(this.cache.keys()); } /** @@ -103,9 +103,7 @@ class LRUCache { * @returns {Array} Array of values */ values() { - // TODO: Return array of values - - throw new Error("Not implemented"); + return Array.from(this.cache.values()); } } diff --git a/11-singleton/index.js b/11-singleton/index.js index 3c98305..1bf13f3 100644 --- a/11-singleton/index.js +++ b/11-singleton/index.js @@ -8,31 +8,35 @@ * A class that only allows one instance to exist. */ class Singleton { - // TODO: Implement Singleton - // Step 1: Create a static property to hold the instance - // static instance = null; + static instance = null; + static _allowConstruction = false; // Step 2: Create a getInstance static method // - Check if instance exists // - If not, create it // - Return the instance - static getInstance() { - // TODO: Implement getInstance - throw new Error("Not implemented"); + if (this.instance === null) { + this._allowConstruction = true; + this.instance = new Singleton(); + this._allowConstruction = false; + } + + return this.instance; } // Step 3: Optionally prevent direct instantiation - // constructor() { - // if (Singleton.instance) { - // throw new Error('Use Singleton.getInstance()'); - // } - // } + constructor() { + if (!Singleton._allowConstruction) { + throw new Error('Use Singleton.getInstance()'); + } + } // Step 4: Add a reset method for testing static resetInstance() { - // TODO: Reset the instance to null + this.instance = null; + this._allowConstruction = false; } } @@ -45,10 +49,8 @@ class Singleton { * @returns {Object} Object with getInstance method */ function createSingleton(Class) { - // TODO: Implement createSingleton - // Step 1: Create a closure variable to hold the instance - // let instance = null; + let instance = null; // Step 2: Return an object with getInstance method // getInstance should: @@ -57,14 +59,14 @@ function createSingleton(Class) { // - Return the same instance on subsequent calls // Step 3: Optionally add resetInstance method - return { getInstance: (...args) => { - // TODO: Implement - throw new Error("Not implemented"); + instance = instance ?? new Class(...args); + + return instance; }, resetInstance: () => { - // TODO: Implement + instance = null; }, }; } diff --git a/12-factory-pattern/index.js b/12-factory-pattern/index.js index 3e0bd14..9bb889e 100644 --- a/12-factory-pattern/index.js +++ b/12-factory-pattern/index.js @@ -61,8 +61,6 @@ class Triangle { * A factory object that creates shapes based on type. */ const ShapeFactory = { - // TODO: Implement create method - /** * Create a shape instance * @param {string} type - Shape type ('circle', 'rectangle', 'triangle') @@ -70,12 +68,18 @@ const ShapeFactory = { * @returns {Object} Shape instance */ create(type, options) { - // TODO: Implement factory logic - // Use switch or object lookup to create the right shape // Throw error for unknown types - - return null; // Replace with implementation + switch (type) { + case 'circle': + return new Circle(options); + case 'rectangle': + return new Rectangle(options); + case 'triangle': + return new Triangle(options); + default: + throw new Error(`Unknown shape: ${type}`); + } }, }; @@ -86,8 +90,7 @@ const ShapeFactory = { */ class Factory { constructor() { - // TODO: Initialize registry - // this.registry = new Map(); + this.registry = new Map(); } /** @@ -99,8 +102,16 @@ class Factory { * @param {Function} [options.validate] - Validation function */ register(type, Class, options = {}) { - // TODO: Implement register // Store the class and options in the registry + this.registry.set(type, { + Class, + options: { + required: options.required || [], + validate: options.validate || (() => true), + }, + }); + + return this; } /** @@ -109,9 +120,7 @@ class Factory { * @returns {boolean} true if type was registered */ unregister(type) { - // TODO: Implement unregister - - throw new Error("Not implemented"); + return this.registry.delete(type); } /** @@ -121,19 +130,34 @@ class Factory { * @returns {Object} Instance of the type */ create(type, args = {}) { - // TODO: Implement create - // Step 1: Check if type is registered - + if(this.registry.has(type)) { // Step 2: Get the class and options + const { Class, options } = this.registry.get(type); // Step 3: Validate required fields (if specified) - + if(options.required && options.required.length > 0) { + for (const field of options.required) { + if (args[field] === undefined) { + throw new Error(`Missing required field: ${field}`); + } + } + } + // Step 4: Run custom validation (if specified) + if(options.validate) { + if(!options.validate(args)){ + throw new Error( + `Validation failed for args: ${Object.keys(args).join(', ')}` + ); + } + } + + // Step 5: Create and return instance + return new Class(args); + } - // Step 5: Create and return instance - - return null; // Replace with implementation + throw new Error(`Unregistered shape: ${type}`); } /** @@ -142,9 +166,7 @@ class Factory { * @returns {boolean} */ has(type) { - // TODO: Implement has - - throw new Error("Not implemented"); + return this.registry.has(type); } /** @@ -152,17 +174,14 @@ class Factory { * @returns {string[]} */ getTypes() { - // TODO: Implement getTypes - - throw new Error("Not implemented"); + return [...this.registry.keys()]; } /** * Clear all registered types */ - clear() { - // TODO: Implement clear - throw new Error("Not implemented"); + clear() { + this.registry.clear(); } } diff --git a/13-decorator-pattern/index.js b/13-decorator-pattern/index.js index 6a3646f..fbee6d3 100644 --- a/13-decorator-pattern/index.js +++ b/13-decorator-pattern/index.js @@ -11,22 +11,22 @@ * @returns {Function} Decorated function */ function withLogging(fn) { - // TODO: Implement withLogging - // Step 1: Return a new function that wraps fn + return function (...args) { + // Step 2: Log the function name and arguments + const functionName = fn.name || "anonymous"; + console.log(`Calling ${functionName} with args: ${JSON.stringify(args)}`); - // Step 2: Log the function name and arguments - - // Step 3: Call the original function - - // Step 4: Log the return value + // Step 3: Call the original function + const value = fn.apply(this, args); - // Step 5: Return the result + // Step 4: Log the return value + console.log(`${functionName} returned: ${value}`); + // Step 5: Return the result + return value; + }; // Note: Preserve 'this' context using apply/call - - // Broken: throws error - throw new Error("Not implemented"); } /** @@ -38,19 +38,22 @@ function withLogging(fn) { * @returns {Function} Decorated function */ function withTiming(fn) { - // TODO: Implement withTiming - // Step 1: Return a new function + return function (...args) { + // Step 2: Record start time (performance.now() or Date.now()) + const start = Date.now(); - // Step 2: Record start time (performance.now() or Date.now()) - - // Step 3: Call original function - - // Step 4: Calculate and log duration + // Step 3: Call original function + const value = fn.apply(this, args); - // Step 5: Return result + // Step 4: Calculate and log duration + const functionName = fn.name || "anonymous"; + const duration = Date.now() - start; + console.log(`${functionName} took ${duration}ms`); - return () => undefined; // Broken placeholder + // Step 5: Return result + return value; + }; } /** @@ -63,20 +66,29 @@ function withTiming(fn) { * @returns {Function} Decorated function */ function withRetry(fn, maxRetries = 3) { - // TODO: Implement withRetry - // Step 1: Return a new function - - // Step 2: Track attempt count - - // Step 3: Loop up to maxRetries: - // - Try to call fn - // - On success, return result - // - On failure, increment attempts and continue - - // Step 4: If all retries fail, throw the last error - - return () => undefined; // Broken placeholder + return function (...args) { + // Step 2: Track attempt count + + let lastError; + + // Step 3: Loop up to maxRetries: + // - Try to call fn + // - On success, return result + // - On failure, increment attempts and continue + for (let attempt = 0; attempt < maxRetries + 1; attempt++) { + try { + const result = fn.apply(this, args); + + return result; + } catch (e) { + lastError = e; + } + } + + // Step 4: If all retries fail, throw the last error + throw lastError; + }; } /** @@ -88,11 +100,20 @@ function withRetry(fn, maxRetries = 3) { * @returns {Function} Decorated function with cache */ function withMemoize(fn) { - // TODO: Implement withMemoize - // Similar to memoization assignment but as a decorator + const cache = new Map(); + + return function (...args) { + const key = JSON.stringify(args); - return () => undefined; // Broken placeholder + if (cache.has(key)) { + return cache.get(key); + } + + const value = fn.apply(this, args); + cache.set(key, value); + return value; + }; } /** @@ -105,17 +126,17 @@ function withMemoize(fn) { * @returns {Function} Decorated function */ function withValidation(fn, validator) { - // TODO: Implement withValidation - // Step 1: Return a new function - - // Step 2: Call validator with arguments - - // Step 3: If validation fails, throw error - - // Step 4: If passes, call original function - - return () => undefined; // Broken placeholder + return function (...args) { + // Step 2: Call validator with arguments + if (!validator(...args)) { + // Step 3: If validation fails, throw error + throw new Error("Validation failed"); + } + + // Step 4: If passes, call original function + return fn.apply(this, args); + }; } /** @@ -128,18 +149,28 @@ function withValidation(fn, validator) { * @returns {Object} Object with cached method */ function withCache(obj, methodName) { - // TODO: Implement withCache - // Step 1: Get the original method + const method = obj[methodName]; // Step 2: Create a cache (Map) + const cache = new Map(); + + function cacheWrapper(...args) { + const key = methodName + JSON.stringify(args); + + if (cache.has(key)) { + return cache.get(key); + } + + const result = method.apply(this, args); + cache.set(key, result); + return result; + } // Step 3: Replace the method with a caching wrapper + obj[methodName] = cacheWrapper; // Step 4: Return the object - - // Broken: deletes the method instead of caching it - delete obj[methodName]; return obj; } @@ -153,15 +184,11 @@ function withCache(obj, methodName) { * @returns {Function} Composed decorator */ function compose(...decorators) { - // TODO: Implement compose - // Return a function that takes fn and applies all decorators - - // Example: compose(a, b, c)(fn) = a(b(c(fn))) - - return (fn) => { - throw new Error("Not implemented"); + return function (fn) { + return decorators.reduceRight((wrappedFn, decorator) => decorator(wrappedFn), fn); }; + // Example: compose(a, b, c)(fn) = a(b(c(fn))) } /** @@ -173,12 +200,9 @@ function compose(...decorators) { * @returns {Function} Piped decorator */ function pipe(...decorators) { - // TODO: Implement pipe - // Same as compose but left-to-right - - return (fn) => { - throw new Error("Not implemented"); + return function (fn) { + return decorators.reduce((wrappedFn, decorator) => decorator(wrappedFn), fn); }; } diff --git a/14-middleware-pipeline/index.js b/14-middleware-pipeline/index.js index 8db7620..0180ccf 100644 --- a/14-middleware-pipeline/index.js +++ b/14-middleware-pipeline/index.js @@ -5,8 +5,7 @@ */ class Pipeline { constructor() { - // TODO: Initialize middleware array - // this.middleware = []; + this.middleware = []; } /** @@ -15,15 +14,15 @@ class Pipeline { * @returns {Pipeline} this (for chaining) */ use(fn) { - // TODO: Implement use - // Step 1: Validate fn is a function + if (!(fn instanceof Function)) + throw new TypeError("Middleware must be a function"); // Step 2: Add to middleware array + this.middleware.push(fn); // Step 3: Return this for chaining - - return null; // Broken: should return this + return this; } /** @@ -32,21 +31,38 @@ class Pipeline { * @returns {Promise} Resolves when pipeline completes */ run(context) { - // TODO: Implement run - // Step 1: Create a dispatch function that: // - Takes an index // - Gets middleware at that index // - If no middleware, resolve // - Otherwise, call middleware with context and next function // - next = () => dispatch(index + 1) + const dispatch = index => { + const middleware = this.middleware[index]; + if (!middleware) + return Promise.resolve(); + + let called = false; + const next = () => { + if (called) { + return Promise.reject(new Error("next() called multiple times")); + } + called = true; + return dispatch(index + 1); + }; + + try { + const result = middleware(context, next); + return Promise.resolve(result); + } catch (e) { + return Promise.reject(e); + } + }; // Step 2: Start dispatch at index 0 + return dispatch(0); // Step 3: Return promise for async support - - // Broken: rejects instead of resolving - return Promise.reject(new Error("Not implemented")); } /** @@ -54,8 +70,6 @@ class Pipeline { * @returns {Function} Composed middleware function */ compose() { - // TODO: Implement compose - // Return a function that takes context and runs the pipeline return (context) => this.run(context); @@ -71,9 +85,9 @@ class Pipeline { * @returns {Function} Composed function (context) => Promise */ function compose(middleware) { - // TODO: Implement compose - // Validate all items are functions + if (!middleware.every(fn => fn instanceof Function)) + throw new Error("All items in middleware array must be functions"); // Return a function that: // - Takes context @@ -82,16 +96,31 @@ function compose(middleware) { return function (context) { function dispatch(index) { - // TODO: Implement dispatch - // Step 1: Get middleware at index + const mw = middleware[index]; // Step 2: If none, return resolved promise + if (!mw) + return Promise.resolve(); + // Step 3: Create next function = () => dispatch(index + 1) // Step 4: Call middleware with (context, next) - // Step 5: Return as promise - - // Broken: rejects instead of resolving - return Promise.reject(new Error("Not implemented")); + // Step 5: Handle multiple next() calls + let called = false; + const next = () => { + if (called) { + return Promise.reject(new Error("next() called multiple times")); + } + called = true; + return dispatch(index + 1); + }; + + try { + const result = mw(context, next); + // Step 6: Return as promise + return Promise.resolve(result); + } catch (e) { + return Promise.reject(e); + } } return dispatch(0); @@ -106,15 +135,17 @@ function compose(middleware) { * @returns {Function} Conditional middleware */ function when(condition, middleware) { - // TODO: Implement when - // Return middleware that: // - Checks condition(ctx) // - If true, runs middleware // - If false, just calls next() return (ctx, next) => { - throw new Error("Not implemented"); + if (condition(ctx)) { + return middleware(ctx, next); + } else { + return next(); + } }; } @@ -125,14 +156,16 @@ function when(condition, middleware) { * @returns {Function} Error handling middleware */ function errorMiddleware(errorHandler) { - // TODO: Implement errorMiddleware - // Return middleware that: // - Wraps next() in try/catch // - Calls errorHandler if error thrown return async (ctx, next) => { - throw new Error("Not implemented"); + try { + return await next(); + } catch (e) { + return errorHandler(e, ctx); + } }; } diff --git a/15-dependency-injection/index.js b/15-dependency-injection/index.js index e4995b5..6a202bf 100644 --- a/15-dependency-injection/index.js +++ b/15-dependency-injection/index.js @@ -3,8 +3,7 @@ */ class Container { constructor() { - // TODO: Initialize registry - // this.registry = new Map(); + this.registry = new Map(); } /** @@ -16,9 +15,15 @@ class Container { * @param {boolean} [options.singleton=false] - Whether to create singleton */ register(name, Class, dependencies = [], options = {}) { - // TODO: Implement register // Store in registry: // { type: 'class', Class, dependencies, singleton, instance: null } + this.registry.set(name, { + type: "class", + Class, + dependencies, + singleton: options.singleton || false, + instance: null, + }); } /** @@ -27,9 +32,13 @@ class Container { * @param {*} instance - Instance to register */ registerInstance(name, instance) { - // TODO: Implement registerInstance // Store in registry: // { type: 'instance', instance } + this.registry.set(name, { + type: "instance", + instance: instance, + singleton: true, + }); } /** @@ -40,9 +49,15 @@ class Container { * @param {Object} [options={}] - Registration options */ registerFactory(name, factory, dependencies = [], options = {}) { - // TODO: Implement registerFactory // Store in registry: // { type: 'factory', factory, dependencies, singleton, instance: null } + this.registry.set(name, { + type: "factory", + factory, + dependencies, + singleton: options.singleton || false, + instance: null, + }); } /** @@ -52,17 +67,55 @@ class Container { * @returns {*} The resolved instance */ resolve(name, resolutionStack = new Set()) { - // TODO: Implement resolve - // Step 1: Check if service is registered // Throw error if not found + if (!this.registry.has(name)) + throw new Error(`Service '${name}' is not registered`); // Step 2: Check for circular dependencies // If name is already in resolutionStack, throw error + if (resolutionStack.has(name)) + throw new Error(`Circular dependency detected: ${Array.from(resolutionStack).join(" => ")} => ${name}`); // Step 3: Get registration from registry + const registration = this.registry.get(name); + + let instance; // Step 4: Handle different types: + switch (registration.type) { + case "instance": + instance = registration.instance; + break; + case "class": + case "factory": + if (registration.singleton && registration.instance) { + instance = registration.instance; + } else { + resolutionStack.add(name); + try { + const resolvedDependencies = registration.dependencies.map(dep => + this.resolve(dep, resolutionStack) + ); + + if (registration.type === "class") { + instance = new registration.Class(...resolvedDependencies); + } else { + instance = registration.factory(...resolvedDependencies); + } + } finally { + resolutionStack.delete(name); + } + } + break; + default: + throw new Error(`Unknown registration type: ${registration.type}`); + } + + if (registration.singleton && !registration.instance) + registration.instance = instance; + + return instance; // For 'instance': // - Return the stored instance @@ -75,9 +128,6 @@ class Container { // - Remove name from resolutionStack // - If singleton, cache instance // - Return instance - - // Broken: returns undefined (causes test assertions to fail) - return undefined; } /** @@ -86,8 +136,7 @@ class Container { * @returns {boolean} */ has(name) { - // TODO: Implement has - throw new Error("Not implemented"); + return this.registry.has(name); } /** @@ -96,16 +145,14 @@ class Container { * @returns {boolean} true if was registered */ unregister(name) { - // TODO: Implement unregister - throw new Error("Not implemented"); + return this.registry.delete(name); } /** * Clear all registrations */ clear() { - // TODO: Implement clear - throw new Error("Not implemented"); + this.registry.clear(); } /** @@ -113,8 +160,7 @@ class Container { * @returns {string[]} */ getRegistrations() { - // TODO: Implement getRegistrations - throw new Error("Not implemented"); + return [...this.registry.keys()]; } } @@ -125,14 +171,28 @@ class Container { * @returns {Container} Child container */ function createChildContainer(parent) { - // TODO: Implement createChildContainer - // Create a new container that: // - First checks its own registry // - Falls back to parent for unregistered services const child = new Container(); + const originalResolve = child.resolve.bind(child); + const originalHas = child.has.bind(child); + // Override resolve to check parent... + child.resolve = (name, resolutionStack = new Set()) => { + if (child.registry.has(name)) { + return originalResolve(name, resolutionStack); + } + + return parent.resolve(name, resolutionStack); + }; + + // Override has to check parent as well + child.has = (name) => { + return originalHas(name) || parent.has(name); + }; + return child; } diff --git a/16-state-machine/index.js b/16-state-machine/index.js index d0bad18..83f78e6 100644 --- a/16-state-machine/index.js +++ b/16-state-machine/index.js @@ -10,13 +10,20 @@ class StateMachine { * @param {Object} [config.context] - Initial context data */ constructor(config) { - // TODO: Implement constructor // Step 1: Validate config has initial and states + if (!config.initial || !config.states) + throw new Error("Invalid configuration: missing 'initial' or 'states' property"); + + // Step 2: Store configuration - // this.config = config; - // this.currentState = config.initial; - // this.context = config.context || {}; + this.config = config; + this.currentState = config.initial; + this.context = config.context || {}; + // Step 3: Validate initial state exists in states + if (!config.states[config.initial]) + throw new Error(`Invalid initial state: ${config.initial} does not exist in states`); + } /** @@ -24,8 +31,7 @@ class StateMachine { * @returns {string} */ get state() { - // TODO: Return current state - throw new Error("Not implemented"); + return this.currentState; } /** @@ -35,41 +41,75 @@ class StateMachine { * @returns {boolean} Whether transition was successful */ transition(event, payload) { - // TODO: Implement transition - // Step 1: Get current state config + const currentStateConfig = this.config.states[this.currentState]; // Step 2: Check if event is valid for current state // Return false if not + if (!currentStateConfig.on || !currentStateConfig.on[event]) + return false; // Step 3: Get transition config (can be string or object) // If string: target = transition // If object: { target, guard, action } - - // Step 4: Check guard if present - // If guard returns false, return false - - // Step 5: Update state to target - - // Step 6: Call action if present + const transitionConfig = currentStateConfig.on[event]; + if (typeof transitionConfig === "string") { + const target = transitionConfig; + if (!this.config.states[target]) + throw new Error(`Target state '${target}' does not exist`); + + this.currentState = target; + } else { + const { target, guard, action } = transitionConfig; + if (!this.config.states[target]) + throw new Error(`Target state '${target}' does not exist`); + + // Step 4: Check guard if present + // If guard returns false, return false + if (guard && !guard(this.context, payload)) + return false; + + // Step 5: Update state to target + this.currentState = target; + + // Step 6: Call action if present + if (action) + action(this.context, payload); + } // Step 7: Return true - - throw new Error("Not implemented"); + return true; } /** * Check if a transition is possible * @param {string} event - Event name + * @param {Object} [payload] - Optional payload for guard evaluation * @returns {boolean} */ - can(event) { - // TODO: Implement can - + can(event, payload) { // Check if event exists for current state // Check guard if present + const currentStateConfig = this.config.states[this.currentState]; + + if (!currentStateConfig.on || !currentStateConfig.on[event]) + return false; - throw new Error("Not implemented"); + const transitionConfig = currentStateConfig.on[event]; + + if (typeof transitionConfig === "string") { + if (!this.config.states[transitionConfig]) + throw new Error(`Target state '${transitionConfig}' does not exist`); + + return true; + } + + const { guard } = transitionConfig; + + if (guard) + return guard(this.context, payload); + + return true; } /** @@ -77,11 +117,10 @@ class StateMachine { * @returns {string[]} Array of event names */ getAvailableTransitions() { - // TODO: Implement getAvailableTransitions - // Return array of event names from current state's 'on' config + const currentStateConfig = this.config.states[this.currentState]; - throw new Error("Not implemented"); + return Object.keys(currentStateConfig.on || {}); } /** @@ -89,8 +128,7 @@ class StateMachine { * @returns {Object} */ getContext() { - // TODO: Return context - throw new Error("Not implemented"); + return this.context; } /** @@ -98,9 +136,13 @@ class StateMachine { * @param {Object|Function} updater - New context or updater function */ updateContext(updater) { - // TODO: Implement updateContext // If updater is function: this.context = updater(this.context) - // If updater is object: merge with existing context + if (typeof updater === "function") { + this.context = updater(this.context); + } else { + // If updater is object: merge with existing context + this.context = { ...this.context, ...updater }; + } } /** @@ -108,8 +150,13 @@ class StateMachine { * @returns {boolean} */ isFinal() { - // TODO: Check if current state has no transitions - throw new Error("Not implemented"); + const transitions = this.config.states[this.currentState].on; + + return ( + !transitions || + (Array.isArray(transitions) && transitions.length === 0) || + (typeof transitions === "object" && Object.keys(transitions).length === 0) + ); } /** @@ -117,8 +164,13 @@ class StateMachine { * @param {Object} [newContext] - Optional new context */ reset(newContext) { - // TODO: Reset to initial state - // Optionally reset context + this.currentState = this.config.initial; + + if (newContext !== undefined) { + this.context = typeof newContext === "object" && newContext !== null + ? { ...newContext } + : newContext; + } } } @@ -129,8 +181,6 @@ class StateMachine { * @returns {Function} Factory function that creates machines */ function createMachine(config) { - // TODO: Implement createMachine - // Return a function that creates new StateMachine instances // with the given config diff --git a/17-command-pattern/index.js b/17-command-pattern/index.js index e8cd005..d22ad8e 100644 --- a/17-command-pattern/index.js +++ b/17-command-pattern/index.js @@ -9,9 +9,8 @@ */ class CommandManager { constructor() { - // TODO: Initialize stacks - // this.undoStack = []; - // this.redoStack = []; + this.undoStack = []; + this.redoStack = []; } /** @@ -19,10 +18,17 @@ class CommandManager { * @param {Object} command - Command with execute() method */ execute(command) { - // TODO: Implement execute + if (!command || typeof command.execute !== "function") + throw new Error("Command must have execute() method"); + // Step 1: Call command.execute() + command.execute(); + // Step 2: Push to undo stack + this.undoStack.push(command); + // Step 3: Clear redo stack (new action invalidates redo history) + this.redoStack.length = 0; } /** @@ -30,19 +36,21 @@ class CommandManager { * @returns {boolean} Whether undo was performed */ undo() { - // TODO: Implement undo - // Step 1: Check if undo stack is empty + if(!this.undoStack.length) + return false; // Step 2: Pop command from undo stack + const command = this.undoStack.pop(); // Step 3: Call command.undo() + command.undo() // Step 4: Push to redo stack + this.redoStack.push(command); // Step 5: Return true - - throw new Error("Not implemented"); + return true; } /** @@ -50,19 +58,21 @@ class CommandManager { * @returns {boolean} Whether redo was performed */ redo() { - // TODO: Implement redo - // Step 1: Check if redo stack is empty + if(!this.redoStack.length) + return false; // Step 2: Pop command from redo stack + const command = this.redoStack.pop(); // Step 3: Call command.execute() + command.execute() // Step 4: Push to undo stack + this.undoStack.push(command); // Step 5: Return true - - throw new Error("Not implemented"); + return true; } /** @@ -70,8 +80,7 @@ class CommandManager { * @returns {boolean} */ canUndo() { - // TODO: Return whether undo stack has items - throw new Error("Not implemented"); + return !!this.undoStack.length; } /** @@ -79,8 +88,7 @@ class CommandManager { * @returns {boolean} */ canRedo() { - // TODO: Return whether redo stack has items - throw new Error("Not implemented"); + return !!this.redoStack.length; } /** @@ -88,15 +96,15 @@ class CommandManager { * @returns {Object[]} */ get history() { - // TODO: Return copy of undo stack - throw new Error("Not implemented"); + return [...this.undoStack]; } /** * Clear all history */ clear() { - // TODO: Clear both stacks + this.undoStack.length = 0; + this.redoStack.length = 0; } } @@ -105,18 +113,17 @@ class CommandManager { */ class AddCommand { constructor(calculator, value) { - // TODO: Store calculator and value - // this.calculator = calculator; - // this.value = value; + this.calculator = calculator; + this.value = value; this.description = `Add ${value}`; } execute() { - // TODO: Add value to calculator.value + this.calculator.value += this.value; } undo() { - // TODO: Subtract value from calculator.value + this.calculator.value -= this.value; } } @@ -125,16 +132,17 @@ class AddCommand { */ class SubtractCommand { constructor(calculator, value) { - // TODO: Store calculator and value + this.calculator = calculator; + this.value = value; this.description = `Subtract ${value}`; } execute() { - // TODO: Subtract value from calculator.value + this.calculator.value -= this.value; } undo() { - // TODO: Add value to calculator.value + this.calculator.value += this.value; } } @@ -143,17 +151,20 @@ class SubtractCommand { */ class MultiplyCommand { constructor(calculator, value) { - // TODO: Store calculator, value, and previous value for undo + this.calculator = calculator; + this.value = value; + this.previousValue = null; this.description = `Multiply by ${value}`; } execute() { - // TODO: Multiply calculator.value by value // Save previous value for undo + this.previousValue = this.calculator.value; + this.calculator.value *= this.value; } undo() { - // TODO: Restore previous value + this.calculator.value = this.previousValue; } } @@ -162,17 +173,23 @@ class MultiplyCommand { */ class DivideCommand { constructor(calculator, value) { - // TODO: Store calculator, value, and previous value for undo + if (value === 0) + throw new Error('Division by zero is not allowed'); + + this.calculator = calculator; + this.value = value; + this.previousValue = null; this.description = `Divide by ${value}`; } execute() { - // TODO: Divide calculator.value by value // Save previous value for undo + this.previousValue = this.calculator.value; + this.calculator.value /= this.value; } undo() { - // TODO: Restore previous value + this.calculator.value = this.previousValue; } } @@ -183,8 +200,7 @@ class DivideCommand { */ class MacroCommand { constructor(commands = []) { - // TODO: Store commands array - // this.commands = commands; + this.commands = commands; this.description = "Macro"; } @@ -193,15 +209,15 @@ class MacroCommand { * @param {Object} command */ add(command) { - // TODO: Add command to array + this.commands.push(command); } execute() { - // TODO: Execute all commands in order + this.commands.forEach(command => command.execute()); } undo() { - // TODO: Undo all commands in reverse order + [...this.commands].reverse().forEach(command => command.undo()); } } @@ -212,16 +228,19 @@ class MacroCommand { */ class SetValueCommand { constructor(calculator, value) { - // TODO: Store calculator, new value, and previous value + this.calculator = calculator; + this.value = value; + this.previousValue = null; this.description = `Set to ${value}`; } execute() { - // TODO: Save previous, set new value + this.previousValue = this.calculator.value; + this.calculator.value = this.value; } undo() { - // TODO: Restore previous value + this.calculator.value = this.previousValue; } } diff --git a/18-strategy-pattern/index.js b/18-strategy-pattern/index.js index 807bcd2..e7cc997 100644 --- a/18-strategy-pattern/index.js +++ b/18-strategy-pattern/index.js @@ -13,18 +13,19 @@ */ class SortContext { constructor(strategy) { - // TODO: Store strategy - // this.strategy = strategy; + this.strategy = strategy; } setStrategy(strategy) { - // TODO: Update strategy + this.strategy = strategy; } sort(array) { - // TODO: Delegate to strategy // Return sorted copy, don't mutate original - throw new Error("Not implemented"); + if (!this.strategy) + throw new Error("No sorting strategy set"); + + return this.strategy.sort([...array]); } } @@ -33,10 +34,19 @@ class SortContext { */ class BubbleSort { sort(array) { - // TODO: Implement bubble sort // Return new sorted array - - return ["NOT_IMPLEMENTED"]; // Broken: Replace with implementation + const sorted = [...array]; + const sortedLength = sorted.length; + + for (let i = 0; i < sortedLength - 1; i++) { + for (let j = 0; j < sortedLength - 1 - i; j++) { + if (sorted[j] > sorted[j + 1]) { + [sorted[j], sorted[j + 1]] = [sorted[j + 1], sorted[j]]; + } + } + } + + return sorted; } } @@ -45,10 +55,20 @@ class BubbleSort { */ class QuickSort { sort(array) { - // TODO: Implement quick sort // Return new sorted array - return []; // Broken: Replace with implementation + if (array.length <= 1) return [...array]; + + const pivot = array[0]; + const left = []; + const right = []; + + for (let i = 1; i < array.length; i++) { + if (array[i] < pivot) left.push(array[i]); + else right.push(array[i]); + } + + return [...this.sort(left), pivot, ...this.sort(right)]; } } @@ -57,10 +77,47 @@ class QuickSort { */ class MergeSort { sort(array) { - // TODO: Implement merge sort - // Return new sorted array + if (array.length <= 1) { + return [...array]; + } + + const middle = Math.floor(array.length / 2); + + const left = array.slice(0, middle); + const right = array.slice(middle); + + const sortedLeft = this.sort(left); + const sortedRight = this.sort(right); - return []; // Broken: Replace with implementation + return this.merge(sortedLeft, sortedRight); + } + + merge(left, right) { + const result = []; + let leftIndex = 0; + let rightIndex = 0; + + while (leftIndex < left.length && rightIndex < right.length) { + if (left[leftIndex] < right[rightIndex]) { + result.push(left[leftIndex]); + leftIndex++; + } else { + result.push(right[rightIndex]); + rightIndex++; + } + } + + while (leftIndex < left.length) { + result.push(left[leftIndex]); + leftIndex++; + } + + while (rightIndex < right.length) { + result.push(right[rightIndex]); + rightIndex++; + } + + return result; } } @@ -75,16 +132,15 @@ class MergeSort { */ class PricingContext { constructor(strategy) { - // TODO: Store strategy + this.strategy = strategy; } setStrategy(strategy) { - // TODO: Update strategy + this.strategy = strategy; } calculateTotal(items) { - // TODO: Delegate to strategy - throw new Error("Not implemented"); + return this.strategy.calculate(items); } } @@ -93,8 +149,7 @@ class PricingContext { */ class RegularPricing { calculate(items) { - // TODO: Sum all item prices - throw new Error("Not implemented"); + return items.reduce((sum, { price }) => sum + price, 0); } } @@ -103,14 +158,12 @@ class RegularPricing { */ class PercentageDiscount { constructor(percentage) { - // TODO: Store percentage (0-100) - // this.percentage = percentage; + this.percentage = percentage; } calculate(items) { - // TODO: Apply percentage discount // total * (1 - percentage/100) - throw new Error("Not implemented"); + return items.reduce((sum, { price }) => sum + price, 0) * (1 - this.percentage / 100); } } @@ -119,14 +172,14 @@ class PercentageDiscount { */ class FixedDiscount { constructor(amount) { - // TODO: Store fixed discount amount - // this.amount = amount; + this.amount = amount; } calculate(items) { - // TODO: Subtract fixed amount from total // Don't go below 0 - throw new Error("Not implemented"); + const total = items.reduce((sum, { price }) => sum + price, 0); + + return Math.max(0, total - this.amount); } } @@ -135,9 +188,12 @@ class FixedDiscount { */ class BuyOneGetOneFree { calculate(items) { - // TODO: Every second item is free // Sort by price desc, charge only every other item - throw new Error("Not implemented"); + return [...items] + .sort((a, b) => b.price - a.price) + .reduce((total, { price }, index) => { + return index % 2 === 0 ? total + price : total; + }, 0); } } @@ -148,14 +204,20 @@ class BuyOneGetOneFree { */ class TieredDiscount { constructor(tiers) { - // TODO: Store tiers // tiers = [{ threshold: 100, discount: 10 }, { threshold: 200, discount: 20 }] - // this.tiers = tiers; + this.tiers = [...tiers].sort((a, b) => a.threshold - b.threshold); } calculate(items) { - // TODO: Apply tier discount based on subtotal - throw new Error("Not implemented"); + const subtotal = items.reduce((sum, { price }) => sum + price, 0); + + for (let i = this.tiers.length - 1; i >= 0; i--) { + if (subtotal >= this.tiers[i].threshold) { + return subtotal * (1 - this.tiers[i].discount / 100); + } + } + + return subtotal; } } @@ -168,16 +230,15 @@ class TieredDiscount { */ class ValidationContext { constructor(strategy) { - // TODO: Store strategy + this.strategy = strategy; } setStrategy(strategy) { - // TODO: Update strategy + this.strategy = strategy; } validate(data) { - // TODO: Delegate to strategy - throw new Error("Not implemented"); + return this.strategy.validate(data); } } @@ -191,10 +252,27 @@ class ValidationContext { */ class StrictValidation { validate(data) { - // TODO: Validate that name, email, and age are all present and valid // Return { valid: boolean, errors: string[] } // Example: { valid: false, errors: ["Name is required", "Email is required"] } - throw new Error("Not implemented"); + const { name, email, age } = data; + const errors = []; + + if (!name || typeof name !== "string" || name.trim() === "") { + errors.push("Name is required"); + } + + if (!email || typeof email !== "string" || email.trim() === "") { + errors.push("Email is required"); + } + + if (age === undefined || age === null || typeof age !== "number" || isNaN(age)) { + errors.push("Age is required and must be a number"); + } + + return { + valid: errors.length === 0, + errors, + }; } } @@ -206,9 +284,11 @@ class StrictValidation { */ class LenientValidation { validate(data) { - // TODO: Always return valid: true, errors: [] // This strategy has no validation rules - return { valid: false, errors: ["Not implemented"] }; // Broken: Replace with implementation + return { + valid: true, + errors: [], + }; } } @@ -223,22 +303,19 @@ class LenientValidation { */ class StrategyRegistry { constructor() { - // TODO: Initialize registry map - // this.strategies = new Map(); + this.strategies = new Map(); } register(name, strategy) { - // TODO: Store strategy by name + this.strategies.set(name, strategy); } get(name) { - // TODO: Return strategy by name - throw new Error("Not implemented"); + return this.strategies.get(name) || null; } has(name) { - // TODO: Check if strategy exists - throw new Error("Not implemented"); + return this.strategies.has(name); } } diff --git a/19-proxy-pattern/index.js b/19-proxy-pattern/index.js index 6d2bf2b..ae693de 100644 --- a/19-proxy-pattern/index.js +++ b/19-proxy-pattern/index.js @@ -10,8 +10,6 @@ * @returns {Proxy} Proxy that validates on set */ function createValidatingProxy(target, validators) { - // TODO: Implement validating proxy - // Create a Proxy with a handler that: // - On 'set': check if validator exists for property // - If validator returns false, throw Error @@ -19,20 +17,26 @@ function createValidatingProxy(target, validators) { // - On 'get': return property value normally return new Proxy(target, { + // Check validators[prop](value) if validator exists + // Throw if validation fails + // Set property if passes set(obj, prop, value) { - // TODO: Implement set trap - // Check validators[prop](value) if validator exists - // Throw if validation fails - // Set property if passes + if (validators[prop]) { + if (!validators[prop](value)) { + throw new Error(`Invalid value: ${value} for property: ${prop}`); + } + } - // Broken: doesn't set at all (fails all tests) - return true; + return Reflect.set(obj, prop, value); }, get(obj, prop) { - // TODO: Implement get trap - // Broken: returns wrong value - return "NOT_IMPLEMENTED"; + const value = Reflect.get(obj, prop); + + if (typeof value === "function") + return value.bind(obj); + + return value; }, }); } @@ -45,27 +49,33 @@ function createValidatingProxy(target, validators) { * @returns {Proxy} Proxy that logs all operations */ function createLoggingProxy(target, logger) { - // TODO: Implement logging proxy - return new Proxy(target, { get(obj, prop) { - // TODO: Log 'get' and return value - throw new Error("Not implemented"); + const value = Reflect.get(obj, prop); + logger("get", prop, value); + + return value; }, set(obj, prop, value) { - // TODO: Log 'set' and set value - throw new Error("Not implemented"); + const result = Reflect.set(obj, prop, value); + logger("set", prop, value); + + return result; }, deleteProperty(obj, prop) { - // TODO: Log 'delete' and delete property - throw new Error("Not implemented"); + const result = Reflect.deleteProperty(obj, prop); + logger("delete", prop); + + return result; }, has(obj, prop) { - // TODO: Log 'has' and return result - throw new Error("Not implemented"); + const result = Reflect.has(obj, prop); + logger("has", prop, result); + + return result; }, }); } @@ -78,24 +88,35 @@ function createLoggingProxy(target, logger) { * @returns {Proxy} Proxy that caches method results */ function createCachingProxy(target, methodNames) { - // TODO: Implement caching proxy - - // Create cache storage - // const cache = new Map(); + const cache = new Map(); + const methods = new Set(methodNames); return new Proxy(target, { get(obj, prop) { - // TODO: Implement get trap + // If prop is in methodNames and is a function: + // Return a wrapped function that: + // - Creates cache key from arguments + // - Returns cached result if exists + // - Otherwise, calls original, caches, and returns + + // Otherwise, return property normally + const value = Reflect.get(obj, prop); + + if (!methods.has(prop) || typeof value !== "function") + return value; + + return function (...args) { + const key = `${prop}:${JSON.stringify(args)}`; - // If prop is in methodNames and is a function: - // Return a wrapped function that: - // - Creates cache key from arguments - // - Returns cached result if exists - // - Otherwise, calls original, caches, and returns + if (cache.has(key)) + return cache.get(key); - // Otherwise, return property normally + const result = value.apply(obj, args); - throw new Error("Not implemented"); + cache.set(key, result); + + return result; + }; }, }); } @@ -110,29 +131,33 @@ function createCachingProxy(target, methodNames) { * @returns {Proxy} Proxy that enforces access control */ function createAccessProxy(target, permissions) { - // TODO: Implement access control proxy - const { readable = [], writable = [] } = permissions; + const readableProps = new Set(readable); + const writableProps = new Set(writable); + return new Proxy(target, { get(obj, prop) { - // TODO: Check if prop is in readable - // Throw if not allowed - // Broken: returns wrong value - return "NOT_IMPLEMENTED"; + // Throw if not allowed + if (!readableProps.has(prop)) + throw new Error(`Access denied to read property: ${prop}`); + + return Reflect.get(obj, prop); }, set(obj, prop, value) { - // TODO: Check if prop is in writable - // Throw if not allowed - // Broken: doesn't actually set - return true; + if (!writableProps.has(prop)) + // Throw if not allowed + throw new Error(`Access denied to write property: ${prop}`); + + return Reflect.set(obj, prop, value); }, deleteProperty(obj, prop) { - // TODO: Only allow if in writable - // Broken: doesn't delete - return true; + if (!writableProps.has(prop)) + throw new Error(`Access denied to delete property: ${prop}`); + + return Reflect.deleteProperty(obj, prop); }, }); } @@ -144,8 +169,6 @@ function createAccessProxy(target, permissions) { * @returns {Proxy} Proxy that loads object on first access */ function createLazyProxy(loader) { - // TODO: Implement lazy loading proxy - let instance = null; let loaded = false; @@ -153,17 +176,24 @@ function createLazyProxy(loader) { {}, { get(obj, prop) { - // TODO: Load instance on first access - // if (!loaded) { instance = loader(); loaded = true; } - // return instance[prop] - throw new Error("Not implemented"); + // if (!loaded) { instance = loader(); loaded = true; } + if (!loaded) { + instance = loader(); + loaded = true; + } + + return Reflect.get(instance, prop); }, set(obj, prop, value) { - // TODO: Load instance if needed, then set - throw new Error("Not implemented"); + if (!loaded) { + instance = loader(); + loaded = true; + } + + return Reflect.set(instance, prop, value); }, - }, + } ); } @@ -175,17 +205,21 @@ function createLazyProxy(loader) { * @returns {Proxy} Proxy that notifies on changes */ function createObservableProxy(target, onChange) { - // TODO: Implement observable proxy - return new Proxy(target, { set(obj, prop, value) { - // TODO: Call onChange(prop, value, oldValue) on change - throw new Error("Not implemented"); + const oldValue = obj[prop]; + const result = Reflect.set(obj, prop, value); + onChange(prop, value, oldValue); + + return result; }, deleteProperty(obj, prop) { - // TODO: Call onChange on delete - throw new Error("Not implemented"); + const oldValue = obj[prop]; + const result = Reflect.deleteProperty(obj, prop); + onChange(prop, undefined, oldValue); + + return result; }, }); } diff --git a/20-builder-pattern/index.js b/20-builder-pattern/index.js index 69a1e1a..009ff8e 100644 --- a/20-builder-pattern/index.js +++ b/20-builder-pattern/index.js @@ -9,12 +9,11 @@ */ class QueryBuilder { constructor() { - // TODO: Initialize state - // this.selectCols = []; - // this.fromTable = null; - // this.whereClauses = []; - // this.orderByClauses = []; - // this.limitCount = null; + this.selectCols = []; + this.fromTable = null; + this.whereClauses = []; + this.orderByClauses = []; + this.limitCount = null; } /** @@ -23,8 +22,9 @@ class QueryBuilder { * @returns {QueryBuilder} this */ select(...columns) { - // TODO: Store columns - throw new Error("Not implemented"); + this.selectCols = columns; + + return this; } /** @@ -33,8 +33,9 @@ class QueryBuilder { * @returns {QueryBuilder} this */ from(table) { - // TODO: Store table name - throw new Error("Not implemented"); + this.fromTable = table; + + return this; } /** @@ -45,8 +46,9 @@ class QueryBuilder { * @returns {QueryBuilder} this */ where(column, operator, value) { - // TODO: Store where clause - throw new Error("Not implemented"); + this.whereClauses.push(`${column} ${operator} ${value}`); + + return this; } /** @@ -56,8 +58,9 @@ class QueryBuilder { * @returns {QueryBuilder} this */ orderBy(column, direction = "ASC") { - // TODO: Store order by clause - throw new Error("Not implemented"); + this.orderByClauses.push(`${column} ${direction}`); + + return this; } /** @@ -66,8 +69,9 @@ class QueryBuilder { * @returns {QueryBuilder} this */ limit(count) { - // TODO: Store limit - throw new Error("Not implemented"); + this.limitCount = count; + + return this; } /** @@ -75,9 +79,31 @@ class QueryBuilder { * @returns {string} SQL query string */ build() { - // TODO: Build and return query string - // Format: SELECT cols FROM table WHERE clauses ORDER BY clause LIMIT n - throw new Error("Not implemented"); + // Format: SELECT cols FROM table WHERE clauses ORDER BY clause LIMIT n + const cols = + this.selectCols && this.selectCols.length > 0 + ? this.selectCols.join(", ") + : "*"; + + let query = `SELECT ${cols}`; + + if (this.fromTable) { + query += ` FROM ${this.fromTable}`; + } + + if (this.whereClauses.length > 0) { + query += ` WHERE ${this.whereClauses.join(" AND ")}`; + } + + if (this.orderByClauses.length > 0) { + query += ` ORDER BY ${this.orderByClauses.join(", ")}`; + } + + if (this.limitCount != null) { + query += ` LIMIT ${this.limitCount}`; + } + + return query; } /** @@ -85,8 +111,13 @@ class QueryBuilder { * @returns {QueryBuilder} this */ reset() { - // TODO: Reset all state - throw new Error("Not implemented"); + this.selectCols = []; + this.fromTable = null; + this.whereClauses = []; + this.orderByClauses = []; + this.limitCount = null; + + return this; } } @@ -97,14 +128,13 @@ class QueryBuilder { */ class HTMLBuilder { constructor() { - // TODO: Initialize state - // this.tagName = 'div'; - // this.idAttr = null; - // this.classes = []; - // this.attributes = {}; - // this.innerContent = ''; - // this.children = []; - } + this.tagName = 'div'; + this.idAttr = null; + this.classes = []; + this.attributes = {}; + this.innerContent = ''; + this.children = []; + } /** * Set tag name @@ -112,8 +142,9 @@ class HTMLBuilder { * @returns {HTMLBuilder} this */ tag(name) { - // TODO: Store tag name - throw new Error("Not implemented"); + this.tagName = name; + + return this; } /** @@ -122,8 +153,9 @@ class HTMLBuilder { * @returns {HTMLBuilder} this */ id(id) { - // TODO: Store id - throw new Error("Not implemented"); + this.idAttr = id; + + return this; } /** @@ -132,8 +164,9 @@ class HTMLBuilder { * @returns {HTMLBuilder} this */ class(...classNames) { - // TODO: Store classes - throw new Error("Not implemented"); + this.classes.push(...classNames); + + return this; } /** @@ -143,8 +176,9 @@ class HTMLBuilder { * @returns {HTMLBuilder} this */ attr(name, value) { - // TODO: Store attribute - throw new Error("Not implemented"); + this.attributes[name] = value; + + return this; } /** @@ -153,8 +187,9 @@ class HTMLBuilder { * @returns {HTMLBuilder} this */ content(content) { - // TODO: Store content - throw new Error("Not implemented"); + this.innerContent = content; + + return this; } /** @@ -163,8 +198,9 @@ class HTMLBuilder { * @returns {HTMLBuilder} this */ child(childHtml) { - // TODO: Store child - throw new Error("Not implemented"); + this.children.push(childHtml); + + return this; } /** @@ -172,9 +208,31 @@ class HTMLBuilder { * @returns {string} HTML element string */ build() { - // TODO: Build and return HTML string - // Format: content - throw new Error("Not implemented"); + const tag = this.tagName || "div"; + let parts = [`<${tag}`]; + + if (this.idAttr) + parts.push(` id="${this.idAttr}"`); + + + if (this.classes.length > 0) + parts.push(` class="${this.classes.join(" ")}"`); + + + for (const [name, value] of Object.entries(this.attributes)) { + parts.push(` ${name}="${value}"`); + } + + parts.push(">"); + + const inner = + (this.innerContent || "") + this.children.join(""); + + parts.push(inner); + + parts.push(``); + + return parts.join(""); } /** @@ -182,8 +240,13 @@ class HTMLBuilder { * @returns {HTMLBuilder} this */ reset() { - // TODO: Reset all state - throw new Error("Not implemented"); + this.tagName = "div"; + this.idAttr = null; + this.classes = []; + this.attributes = {}; + this.innerContent = ""; + this.children = []; + return this; } } @@ -194,13 +257,12 @@ class HTMLBuilder { */ class ConfigBuilder { constructor() { - // TODO: Initialize state - // this.config = { - // environment: 'development', - // database: null, - // features: [], - // logLevel: 'info' - // }; + this.config = { + environment: 'development', + database: null, + features: [], + logLevel: 'info', + }; } /** @@ -209,8 +271,9 @@ class ConfigBuilder { * @returns {ConfigBuilder} this */ setEnvironment(env) { - // TODO: Set environment - throw new Error("Not implemented"); + this.config.environment = env; + + return this; } /** @@ -219,8 +282,9 @@ class ConfigBuilder { * @returns {ConfigBuilder} this */ setDatabase(dbConfig) { - // TODO: Set database config - throw new Error("Not implemented"); + this.config.database = dbConfig; + + return this; } /** @@ -229,8 +293,10 @@ class ConfigBuilder { * @returns {ConfigBuilder} this */ enableFeature(feature) { - // TODO: Add feature to list - throw new Error("Not implemented"); + if (!this.config.features.includes(feature)) + this.config.features.push(feature); + + return this; } /** @@ -239,8 +305,10 @@ class ConfigBuilder { * @returns {ConfigBuilder} this */ disableFeature(feature) { - // TODO: Remove feature from list - throw new Error("Not implemented"); + this.config.features = + this.config.features.filter((f) => f !== feature); + + return this; } /** @@ -249,8 +317,9 @@ class ConfigBuilder { * @returns {ConfigBuilder} this */ setLogLevel(level) { - // TODO: Set log level - throw new Error("Not implemented"); + this.config.logLevel = level; + + return this; } /** @@ -258,8 +327,10 @@ class ConfigBuilder { * @returns {Object} Configuration object */ build() { - // TODO: Return copy of config - throw new Error("Not implemented"); + return { + ...this.config, + features: [...this.config.features], + }; } } @@ -270,7 +341,12 @@ class ConfigBuilder { */ class RequestBuilder { constructor(baseUrl = "") { - // TODO: Initialize state + this.baseUrl = baseUrl; + this._method = "GET"; + this._path = ""; + this._query = {}; + this._headers = {}; + this._body = undefined; } /** @@ -279,7 +355,9 @@ class RequestBuilder { * @returns {RequestBuilder} this */ method(method) { - throw new Error("Not implemented"); + this._method = method; + + return this; } /** @@ -288,7 +366,9 @@ class RequestBuilder { * @returns {RequestBuilder} this */ path(path) { - throw new Error("Not implemented"); + this._path = path; + + return this; } /** @@ -298,7 +378,9 @@ class RequestBuilder { * @returns {RequestBuilder} this */ query(key, value) { - throw new Error("Not implemented"); + this._query[key] = value; + + return this; } /** @@ -308,7 +390,9 @@ class RequestBuilder { * @returns {RequestBuilder} this */ header(key, value) { - throw new Error("Not implemented"); + this._headers[key] = value; + + return this; } /** @@ -317,7 +401,9 @@ class RequestBuilder { * @returns {RequestBuilder} this */ body(body) { - throw new Error("Not implemented"); + this._body = body; + + return this; } /** @@ -325,8 +411,22 @@ class RequestBuilder { * @returns {Object} Request config for fetch */ build() { - // TODO: Return fetch-compatible config - throw new Error("Not implemented"); + let url = (this.baseUrl || "") + (this._path || ""); + + const queryKeys = Object.keys(this._query); + + if (queryKeys.length > 0) { + const searchParams = new URLSearchParams(this._query); + + url += `?${searchParams.toString()}`; + } + + return { + method: this._method, + url, + headers: { ...this._headers }, + body: this._body, + }; } }