diff --git a/01-deep-clone/index.js b/01-deep-clone/index.js index e0281df..857f53e 100644 --- a/01-deep-clone/index.js +++ b/01-deep-clone/index.js @@ -9,33 +9,48 @@ * @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 - - // Step 2: Check for circular references using the visited WeakMap - // If we've seen this object before, return the cached clone - - // Step 3: Handle Date objects - // Create a new Date with the same time value - - // Step 4: Handle RegExp objects - // Create a new RegExp with the same source and flags - - // Step 5: Handle Map objects - // Create a new Map and deep clone each key-value pair - - // Step 6: Handle Set objects - // Create a new Set and deep clone each value - - // Step 7: Handle Arrays - // Create a new array and deep clone each element - - // Step 8: Handle plain Objects - // Create a new object and deep clone each property - - return undefined; // Broken: Replace with your implementation + if (typeof (value) !== 'object' || value == null) return value; + + if (visited.has(value)) return visited.get(value); + + switch (Object.prototype.toString.call(value)) { + case '[object Date]': + return new Date(value); + case '[object RegExp]': + return new RegExp(value); + case '[object Map]': + const mapResult = new Map(); + visited.set(value, mapResult); + + for (const [k, v] of value) { + mapResult.set(deepClone(k, visited), deepClone(v, visited)); + } + return mapResult; + case '[object Set]': + const setResult = new Set(); + visited.set(value, setResult); + + for (const v of value) { + setResult.add(deepClone(v,visited)); + } + return setResult; + case '[object Array]': + const arrayResult = []; + visited.set(value, arrayResult); + + for (const i of value) { + arrayResult.push(deepClone(i,visited)); + } + return arrayResult; + default: + const objectResult = {}; + visited.set(value, objectResult); + + for (const p in value) { + objectResult[p] = deepClone(value[p], visited); + } + return objectResult; + } } -module.exports = { deepClone }; +module.exports = {deepClone}; diff --git a/02-debounce-throttle/index.js b/02-debounce-throttle/index.js index 397281b..349e560 100644 --- a/02-debounce-throttle/index.js +++ b/02-debounce-throttle/index.js @@ -9,21 +9,21 @@ * @returns {Function} The debounced function with a cancel() method */ function debounce(fn, delay) { - // TODO: Implement debounce + let timeoutId; - // Step 1: Create a variable to store the timeout ID + const debounced = function (...args) { + const context = this; + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + fn.bind(context)(...args); + }, delay); + }; - // 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 + debounced.cancel = function () { + clearTimeout(timeoutId); + }; - // Step 3: Add a cancel() method to clear pending timeout - - // Step 4: Return the debounced function - - // Return a placeholder that doesn't work - throw new Error("Not implemented"); + return debounced; } /** @@ -37,23 +37,23 @@ function debounce(fn, delay) { * @returns {Function} The throttled function with a cancel() method */ function throttle(fn, limit) { - // TODO: Implement throttle - - // Step 1: Create variables to track: - // - Whether we're currently in a throttle period - // - The timeout ID for cleanup + let inThrottle; + let timeoutId; - // 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 + const throttled = function (...args) { + if (inThrottle) return; - // Step 3: Add a cancel() method to reset throttle state + fn.apply(this, args); + inThrottle = true; + timeoutId = setTimeout(() => inThrottle = false, limit); + }; - // Step 4: Return the throttled function + throttled.cancel = function () { + clearTimeout(timeoutId); + inThrottle = false; + }; - // Return a placeholder that doesn't work - throw new Error("Not implemented"); + return throttled; } -module.exports = { debounce, throttle }; +module.exports = {debounce, throttle}; diff --git a/03-custom-bind/index.js b/03-custom-bind/index.js index 3e691f9..d4eb3cb 100644 --- a/03-custom-bind/index.js +++ b/03-custom-bind/index.js @@ -11,29 +11,20 @@ * @returns {Function} A new bound function */ function customBind(fn, context, ...boundArgs) { - // TODO: Implement custom bind + if (typeof (fn) !== 'function') throw TypeError(); - // Step 1: Validate that fn is a function - // Throw TypeError if not + const bound = function (...args) { + const isNewCall = this instanceof bound; + const finalThis = isNewCall ? this : context; - // 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 + return fn.apply(finalThis, [...boundArgs, ...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 (fn.prototype != null) { + bound.prototype = Object.create(fn.prototype); + } - // Step 4: Preserve the prototype for constructor usage - // boundFunction.prototype = Object.create(fn.prototype) - - // Step 5: Return the bound function - - // Return placeholder that doesn't work - throw new Error("Not implemented"); + return bound; } /** @@ -48,4 +39,4 @@ function customBind(fn, context, ...boundArgs) { // // Your implementation // }; -module.exports = { customBind }; +module.exports = {customBind}; diff --git a/04-memoization/index.js b/04-memoization/index.js index dc0f09b..48e9dd2 100644 --- a/04-memoization/index.js +++ b/04-memoization/index.js @@ -11,47 +11,39 @@ * @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; - - // Step 2: Create the cache (use Map for ordered keys) - // const cache = new Map(); - - // Step 3: Create default key generator - // Default: JSON.stringify(args) or args.join(',') - - // Step 4: Create the memoized function - // - Generate cache key from arguments - // - Check if key exists and is not expired (TTL) - // - If cached, return cached value - // - If not cached, call fn and store result - // - Handle maxSize eviction (remove oldest) - - // 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, - get size() { - return -1; - }, - }; - return memoized; + const {maxSize = 100, ttl = 5000, keyGenerator = (args) => JSON.stringify(args)} = options; + const cache = new Map(); + + const memoized = function (...args) { + const key = keyGenerator(args); + + if (cache.has(key)) { + const cachedValue = cache.get(key); + + if (Date.now() - cachedValue.timestamp < ttl) { + return cachedValue.value; + } + cache.delete(key); + } + + const value = fn.apply(this, args); + cache.set(key, {value: value, timestamp: Date.now()}); + + if (cache.size > maxSize) { + cache.delete(cache.keys().next().value); + } + + return value; + }; + memoized.cache = { + clear: () => cache.clear(), + delete: (key) => cache.delete(key), + has: (key) => cache.has(key), + get size() { + return cache.size; + }, + }; + return memoized; } -module.exports = { memoize }; +module.exports = {memoize}; diff --git a/05-promise-utilities/index.js b/05-promise-utilities/index.js index e59a8eb..54344ed 100644 --- a/05-promise-utilities/index.js +++ b/05-promise-utilities/index.js @@ -8,30 +8,29 @@ * @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); - - // Step 2: Handle empty array case - // Return Promise.resolve([]) for empty input - - // 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 + const promiseArray = Array.from(promises); + + if (promiseArray.length === 0) return Promise.resolve([]); + + return new Promise((resolve, reject) => { + const results = new Array(promiseArray.length); + let completed = 0; + + for (let i = 0; i < promiseArray.length; i++) { + Promise.resolve(promiseArray[i]) + .then((value) => { + results[i] = value; + completed++; + + if (completed === promiseArray.length) { + resolve(results); + } + }).catch(err => { + reject(err); + }); + } + } + ); } /** @@ -43,19 +42,19 @@ 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 - - // Step 2: Handle empty array (return pending promise) - // For empty array, return a promise that never settles - - // 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 + const promiseArray = Array.from(promises); + + if (promiseArray.length === 0) return Promise.race([]); + + return new Promise((resolve, reject) => { + promiseArray.forEach(item => { + Promise.resolve(item).then((value) => { + resolve(value); + }).catch((err) => { + reject(err); + }); + }); + }); } /** @@ -68,24 +67,29 @@ 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 - - // Step 2: Handle empty array case - - // 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 + const promiseArray = Array.from(promises); + + if (promiseArray.length === 0) return Promise.allSettled([]); + + return new Promise((resolve) => { + const result = new Array(promiseArray.length); + let complete = 0; + + promiseArray.forEach((item, index) => { + Promise.resolve(item) + .then(value => { + result[index] = ({status: 'fulfilled', value: value}); + complete++; + if (complete === promiseArray.length) resolve(result); + }) + .catch(err => { + result[index] = ({status: 'rejected', reason: err}); + complete++; + if (complete === promiseArray.length) resolve(result); + }); + }); + + }); } /** @@ -98,27 +102,25 @@ 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 - - // Step 2: Handle empty array (reject with AggregateError) - - // Step 3: Create a new Promise + const promiseArray = Array.from(promises); - // Step 4: Track rejection count and errors - // const errors = []; - // let rejectedCount = 0; + if (promiseArray.length === 0) return Promise.reject(new AggregateError([])); - // 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 + return new Promise((resolve, reject) => { + const errors = []; + let rejectedCount = 0; - // Note: AggregateError is created like: - // new AggregateError(errorsArray, 'All promises were rejected') + promiseArray.forEach(item => { + Promise.resolve(item).then(value => { + resolve(value); + }).catch(err => { + errors[rejectedCount] = err; + rejectedCount++; - return Promise.reject(new AggregateError([], "No promises")); // Replace + if (rejectedCount === promiseArray.length) reject(new AggregateError(errors, 'All promises were rejected')); + }); + }); + }); } -module.exports = { promiseAll, promiseRace, promiseAllSettled, promiseAny }; +module.exports = {promiseAll, promiseRace, promiseAllSettled, promiseAny}; diff --git a/06-async-queue/index.js b/06-async-queue/index.js index 2ab4d0a..73928a5 100644 --- a/06-async-queue/index.js +++ b/06-async-queue/index.js @@ -4,133 +4,137 @@ * A queue that processes async tasks with concurrency control. */ class AsyncQueue { - /** - * Create an async queue - * @param {Object} options - Queue options - * @param {number} [options.concurrency=1] - Maximum concurrent tasks - * @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; - // Step 2: Initialize internal state - // this.queue = []; // Pending tasks - // this.running = 0; // Currently running count - // this.paused = false; // Paused state - // this.emptyCallbacks = []; // Callbacks for empty event - } - - /** - * Add a task to the queue - * @param {Function} task - Async function to execute - * @param {Object} [options] - Task options - * @param {number} [options.priority=0] - Task priority (higher = sooner) - * @returns {Promise} Resolves when task completes - */ - add(task, options = {}) { - // TODO: Implement add - - // Step 1: Create a new Promise and store its resolve/reject - - // Step 2: Create task entry with: task, priority, resolve, reject - - // Step 3: Add to queue (consider priority ordering) - - // Step 4: Try to process if autoStart and not paused - - // Step 5: Return the promise - - return Promise.resolve(); // Replace with your implementation - } - - /** - * Start processing the queue - */ - start() { - // TODO: Implement start - // Set paused to false and trigger processing - } - - /** - * Pause the queue (running tasks will complete) - */ - pause() { - // TODO: Implement pause - // Set paused to true - } - - /** - * Clear all pending tasks - */ - clear() { - // TODO: Implement clear - // Empty the queue array - // Optionally: reject pending promises with an error - } - - /** - * Register callback for when queue becomes empty - * @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 - } - - /** - * Number of pending tasks - * @returns {number} - */ - get size() { - // TODO: Return queue length - throw new Error("Not implemented"); - } - - /** - * Number of currently running tasks - * @returns {number} - */ - get pending() { - // TODO: Return running count - throw new Error("Not implemented"); - } - - /** - * Whether queue is paused - * @returns {boolean} - */ - get isPaused() { - // TODO: Return paused state - throw new Error("Not implemented"); - } - - /** - * Internal: Process next tasks from queue - * @private - */ - _process() { - // TODO: Implement _process - // Step 1: Check if we can run more tasks - // - Not paused - // - Running count < concurrency - // - Queue has items - // Step 2: Take task from queue (respect priority) - // Step 3: Increment running count - // Step 4: Execute task and handle result - // - On success: resolve the task's promise - // - On error: reject the task's promise - // - Always: decrement running, call _process again, check if empty - } - - /** - * Internal: Check and trigger empty callbacks - * @private - */ - _checkEmpty() { - // TODO: If queue is empty and nothing running, call empty callbacks - } + /** + * Create an async queue + * @param {Object} options - Queue options + * @param {number} [options.concurrency=1] - Maximum concurrent tasks + * @param {boolean} [options.autoStart=true] - Start processing immediately + */ + constructor(options = {}) { + this.concurrency = options.concurrency || 1; + this.autoStart = options.autoStart !== false; + this.queue = []; + this.running = 0; + this.paused = false; + this.emptyCallbacks = []; + } + + /** + * Add a task to the queue + * @param {Function} task - Async function to execute + * @param {Object} [options] - Task options + * @param {number} [options.priority=0] - Task priority (higher = sooner) + * @returns {Promise} Resolves when task completes + */ + add(task, options = {}) { + return new Promise((resolve, reject) => { + const entry = { + task: task, priority: options.priority || 1, resolve: resolve, reject: reject + }; + + this.queue.push(entry); + this.queue.sort((a, b) => b.priority - a.priority); + + if (this.autoStart && !this.paused) this._process(); + }); + } + + /** + * Start processing the queue + */ + start() { + this.paused = false; + this._process(); + } + + /** + * Pause the queue (running tasks will complete) + */ + pause() { + this.paused = true; + } + + /** + * Clear all pending tasks + */ + clear() { + this.queue = []; + } + + /** + * Register callback for when queue becomes empty + * @param {Function} callback - Called when queue is empty + */ + onEmpty(callback) { + this.emptyCallbacks.push(callback); + if (this.queue.length === 0 && this.running === 0) this._checkEmpty(); + } + + /** + * Number of pending tasks + * @returns {number} + */ + get size() { + return this.queue.length; + } + + /** + * Number of currently running tasks + * @returns {number} + */ + get pending() { + return this.running; + } + + /** + * Whether queue is paused + * @returns {boolean} + */ + get isPaused() { + return this.paused; + } + + /** + * Internal: Process next tasks from queue + * @private + */ + _process() { + if (this.paused) return; + + while ( + this.running < this.concurrency && + this.queue.length > 0 + ) { + const entry = this.queue.shift(); + this.running++; + + Promise.resolve() + .then(() => entry.task()) + .then(result => { + entry.resolve(result); + }) + .catch(err => { + entry.reject(err); + }) + .finally(() => { + this.running--; + this._process(); + this._checkEmpty(); + }); + } + } + + /** + * Internal: Check and trigger empty callbacks + * @private + */ + _checkEmpty() { + if (this.queue.length === 0 && this.running === 0) { + const callbacks = this.emptyCallbacks; + this.emptyCallbacks = []; + callbacks.forEach(item => item()); + } + } } -module.exports = { AsyncQueue }; +module.exports = {AsyncQueue}; diff --git a/07-retry-with-backoff/index.js b/07-retry-with-backoff/index.js index ed7fd75..1bd760e 100644 --- a/07-retry-with-backoff/index.js +++ b/07-retry-with-backoff/index.js @@ -15,38 +15,36 @@ * @returns {Promise} Result of fn or throws last error */ async function retry(fn, options = {}) { - // TODO: Implement retry with backoff + const { + maxRetries = 3, + initialDelay = 1000, + maxDelay = 30000, + backoff = "exponential", + jitter = false, + retryIf = () => true, + onRetry = () => { + }, + } = options; - // Step 1: Extract options with defaults - // const { - // maxRetries = 3, - // initialDelay = 1000, - // maxDelay = 30000, - // backoff = 'exponential', - // jitter = false, - // retryIf = () => true, - // onRetry = () => {} - // } = options; + let lastError = {}; - // Step 2: Initialize attempt counter and last error + for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { + try { + return await fn(); + } catch (err) { + if (!retryIf(err) || attempt === maxRetries) throw err; - // Step 3: Loop up to maxRetries + 1 (initial attempt + retries) + lastError = err; + onRetry(err, attempt); - // Step 4: Try to execute fn - // - On success: return result - // - On error: check if should retry + let delay = calculateDelay(backoff, attempt, initialDelay); + if (jitter) delay = delay = applyJitter(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 + delay = Math.min(delay, maxDelay); - // Step 6: If all retries exhausted, throw last error - - throw new Error("Not implemented"); // Replace with your implementation + await sleep(delay); + } + } } /** @@ -58,13 +56,14 @@ 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 "fixed": + return initialDelay; + case "linear": + return initialDelay * attempt; + case "exponential": + return initialDelay * Math.pow(2, attempt - 1); + } } /** @@ -74,10 +73,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); } /** @@ -87,7 +83,12 @@ function applyJitter(delay) { * @returns {Promise} Resolves after delay */ function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } -module.exports = { retry, calculateDelay, applyJitter, sleep }; +module.exports = { + retry, + calculateDelay, + applyJitter, + sleep, +}; diff --git a/08-event-emitter/index.js b/08-event-emitter/index.js index b7c9a7c..b890632 100644 --- a/08-event-emitter/index.js +++ b/08-event-emitter/index.js @@ -4,128 +4,115 @@ * A pub/sub event system similar to Node.js EventEmitter. */ class EventEmitter { - constructor() { - // TODO: Initialize event storage - // this.events = new Map(); // or {} - } - - /** - * Register a listener for an event - * @param {string} event - Event name - * @param {Function} listener - Callback function - * @returns {EventEmitter} this (for chaining) - */ - on(event, listener) { - // TODO: Implement on - - // Step 1: Get or create the listeners array for this event - - // Step 2: Add the listener to the array - - // Step 3: Return this for chaining - - return null; // Broken: should return this - } - - /** - * Remove a specific listener for an event - * @param {string} event - Event name - * @param {Function} listener - Callback to remove - * @returns {EventEmitter} this (for chaining) - */ - off(event, listener) { - // TODO: Implement off - - // Step 1: Get the listeners array for this event - - // Step 2: Find and remove the listener - // Note: Handle wrapped 'once' listeners - - // Step 3: Return this for chaining - - return null; // Broken: should return this - } - - /** - * Emit an event, calling all registered listeners - * @param {string} event - Event name - * @param {...*} args - Arguments to pass to listeners - * @returns {boolean} true if event had listeners - */ - emit(event, ...args) { - // TODO: Implement emit - - // Step 1: Get the listeners array for this event - - // Step 2: If no listeners, return false - - // Step 3: Call each listener with the arguments - // Make a copy of the array to handle removals during emit - - // Step 4: Return true - - throw new Error("Not implemented"); - } - - /** - * Register a one-time listener - * @param {string} event - Event name - * @param {Function} listener - Callback function - * @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 - - // Step 2: Store reference to original listener for 'off' to work - - // Step 3: Register the wrapper with 'on' - - // Step 4: Return this for chaining - - return null; // Broken: should return this - } - - /** - * Remove all listeners for an event (or all events) - * @param {string} [event] - Event name (optional) - * @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 - - return null; // Broken: should return this - } - - /** - * Get array of listeners for an event - * @param {string} event - Event name - * @returns {Function[]} Array of listener functions - */ - listeners(event) { - // TODO: Implement listeners - - // Return copy of listeners array, or empty array if none - - throw new Error("Not implemented"); - } - - /** - * Get number of listeners for an event - * @param {string} event - Event name - * @returns {number} Listener count - */ - listenerCount(event) { - // TODO: Implement listenerCount - - throw new Error("Not implemented"); - } + constructor() { + this.events = new Map(); + } + + /** + * Register a listener for an event + * @param {string} event - Event name + * @param {Function} listener - Callback function + * @returns {EventEmitter} this (for chaining) + */ + on(event, listener) { + if (!this.events.has(event)) this.events.set(event, []); + + const listeners = this.events.get(event); + + listeners.push(listener); + + return this; + } + + /** + * Remove a specific listener for an event + * @param {string} event - Event name + * @param {Function} listener - Callback to remove + * @returns {EventEmitter} this (for chaining) + */ + off(event, listener) { + const listeners = this.events.get(event); + + if (listeners == null) return this; + + const index = listeners.findIndex(l => l === listener || l.original === listener); + + if (index !== -1) + listeners.splice(index, 1); + + return this; + } + + /** + * Emit an event, calling all registered listeners + * @param {string} event - Event name + * @param {...*} args - Arguments to pass to listeners + * @returns {boolean} true if event had listeners + */ + emit(event, ...args) { + if (!this.events.has(event)) return false; + + const listeners = this.events.get(event).slice(); + + if (!listeners) return false; + + listeners.forEach(listener => listener(...args)); + + return true; + } + + /** + * Register a one-time listener + * @param {string} event - Event name + * @param {Function} listener - Callback function + * @returns {EventEmitter} this (for chaining) + */ + once(event, listener) { + const self = this; + + function wrapper(...args) { + self.off(event, wrapper); + listener(...args); + } + + wrapper.original = listener; + this.on(event, wrapper); + + return this; + } + + /** + * Remove all listeners for an event (or all events) + * @param {string} [event] - Event name (optional) + * @returns {EventEmitter} this (for chaining) + */ + removeAllListeners(event) { + if (!this.events.has(event)) { + this.events = new Map(); + return this; + } + + this.events.set(event, []); + return this; + } + + /** + * Get array of listeners for an event + * @param {string} event - Event name + * @returns {Function[]} Array of listener functions + */ + listeners(event) { + return Array.of(...this.events.get(event) ?? []); + } + + /** + * Get number of listeners for an event + * @param {string} event - Event name + * @returns {number} Listener count + */ + listenerCount(event) { + return this.events.get(event)?.length || 0; + } } -module.exports = { EventEmitter }; +module.exports = {EventEmitter}; diff --git a/09-observable/index.js b/09-observable/index.js index a7e8f15..dec1859 100644 --- a/09-observable/index.js +++ b/09-observable/index.js @@ -4,133 +4,175 @@ * A simple Observable for reactive data streams. */ class Observable { - /** - * Create an Observable - * @param {Function} subscribeFn - Function called with subscriber on subscribe - */ - constructor(subscribeFn) { - // TODO: Store the subscribe function - // this._subscribeFn = subscribeFn; - } - - /** - * Subscribe to the Observable - * @param {Object|Function} observer - Observer object or next callback - * @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 } - - // Step 2: Create a subscriber object that: - // - Has next, error, complete methods - // - Tracks if completed/errored (stops accepting values) - // - Calls observer methods when appropriate - - // Step 3: Call the subscribe function with the subscriber - - // Step 4: Handle cleanup function returned by subscribeFn - - // Step 5: Return subscription object with unsubscribe method - - throw new Error("Not implemented"); - } - - /** - * Transform each emitted value - * @param {Function} fn - Transform function - * @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 - } - - /** - * Filter emitted values - * @param {Function} predicate - Filter function - * @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 - } - - /** - * Take only first n values - * @param {number} count - Number of values to take - * @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 - } - - /** - * Skip first n values - * @param {number} count - Number of values to skip - * @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 - } - - /** - * Create Observable from array - * @param {Array} array - Array of values - * @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) => { - // subscriber.next(...) for each - // subscriber.complete() - }); - } - - /** - * Create Observable from single value - * @param {*} value - Value to emit - * @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); - } + /** + * Create an Observable + * @param {Function} subscribeFn - Function called with subscriber on subscribe + */ + constructor(subscribeFn) { + this._subscribeFn = subscribeFn; + } + + /** + * Subscribe to the Observable + * @param {Object|Function} observer - Observer object or next callback + * @returns {Object} Subscription with unsubscribe method + */ + subscribe(observer) { + let completed = false; + let errored = false; + let cleanup; + + if (typeof observer === 'function') { + observer = {next: observer}; + } + + const subscriber = { + next: (value) => { + if (completed || errored) return; + + observer.next && observer.next(value); + }, + + error: (err) => { + if (completed || errored) return; + errored = true; + + observer.error && observer.error(err); + cleanup && cleanup(); + }, + + complete: () => { + if (completed || errored) return; + completed = true; + + observer.complete && observer.complete(); + cleanup && cleanup(); + } + }; + cleanup = this._subscribeFn(subscriber); + + return { + unsubscribe() { + if (completed || errored) return; + + completed = errored = true; + cleanup && cleanup(); + } + }; + } + + /** + * Transform each emitted value + * @param {Function} fn - Transform function + * @returns {Observable} New Observable with transformed values + */ + map(fn) { + return new Observable(subscriber => { + const subscription = this.subscribe({ + next: (value) => subscriber.next(fn(value)), + error: (err) => subscriber.error(err), + complete: () => subscriber.complete() + }); + + return () => subscription.unsubscribe(); + }); + } + + /** + * Filter emitted values + * @param {Function} predicate - Filter function + * @returns {Observable} New Observable with filtered values + */ + filter(predicate) { + return new Observable(subscriber => { + const subscription = this.subscribe({ + next: (value) => { + if (predicate(value)) + subscriber.next(value); + }, + error: (err) => subscriber.error(err), + complete: () => subscriber.complete() + }); + + return () => subscription.unsubscribe(); + }); + } + + /** + * Take only first n values + * @param {number} count - Number of values to take + * @returns {Observable} New Observable limited to count values + */ + take(count) { + let step = 1; + + return new Observable(subscriber => { + const subscription = this.subscribe({ + next: (value) => { + if (step > count) { + subscriber.complete(); + return; + } + + subscriber.next(value); + step++; + }, + error: (err) => subscriber.error(err), + complete: () => subscriber.complete() + }); + + return () => subscription.unsubscribe(); + }); + } + + /** + * Skip first n values + * @param {number} count - Number of values to skip + * @returns {Observable} New Observable that skips first count values + */ + skip(count) { + let step = 1; + + return new Observable(subscriber => { + const subscription = this.subscribe({ + next: (value) => { + if (step <= count) { + step++; + return; + } + + subscriber.next(value); + }, + error: (err) => subscriber.error(err), + complete: () => subscriber.complete() + }); + + return () => subscription.unsubscribe(); + }); + } + + /** + * Create Observable from array + * @param {Array} array - Array of values + * @returns {Observable} Observable that emits array values + */ + static from(array) { + return new Observable((subscriber) => { + array.forEach(item => { + subscriber.next(item); + }); + subscriber.complete(); + }); + } + + /** + * Create Observable from single value + * @param {*} value - Value to emit + * @returns {Observable} Observable that emits single value + */ + static of(...values) { + return Observable.from(values); + } } -module.exports = { Observable }; +module.exports = {Observable}; diff --git a/10-lru-cache/index.js b/10-lru-cache/index.js index f087769..3dd7694 100644 --- a/10-lru-cache/index.js +++ b/10-lru-cache/index.js @@ -4,109 +4,96 @@ * A cache that evicts the least recently used items when at capacity. */ class LRUCache { - /** - * Create an LRU Cache - * @param {number} capacity - Maximum number of items - */ - constructor(capacity) { - // TODO: Initialize the cache - // Step 1: Store capacity - // this.capacity = capacity; - // Step 2: Create storage (Map recommended) - // this.cache = new Map(); - } - - /** - * Get value by key - * @param {*} key - Cache key - * @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 - - // Step 3: If not exists, return undefined - - throw new Error("Not implemented"); - } - - /** - * Set key-value pair - * @param {*} key - Cache key - * @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) - } - - /** - * Check if key exists (without updating recency) - * @param {*} key - Cache key - * @returns {boolean} - */ - has(key) { - // TODO: Implement has - - throw new Error("Not implemented"); - } - - /** - * Delete a key - * @param {*} key - Cache key - * @returns {boolean} true if key existed - */ - delete(key) { - // TODO: Implement delete - - throw new Error("Not implemented"); - } - - /** - * Clear all items - */ - clear() { - // TODO: Implement clear - throw new Error("Not implemented"); - } - - /** - * Current number of items - * @returns {number} - */ - get size() { - // TODO: Return current size - - throw new Error("Not implemented"); - } - - /** - * Get all keys in order (least recent first) - * @returns {Array} Array of keys - */ - keys() { - // TODO: Return array of keys - - throw new Error("Not implemented"); - } - - /** - * Get all values in order (least recent first) - * @returns {Array} Array of values - */ - values() { - // TODO: Return array of values - - throw new Error("Not implemented"); - } + /** + * Create an LRU Cache + * @param {number} capacity - Maximum number of items + */ + constructor(capacity) { + this.capacity = capacity; + this.cache = new Map(); + } + + /** + * Get value by key + * @param {*} key - Cache key + * @returns {*} Value or undefined if not found + */ + get(key) { + if (!this.cache.has(key)) return undefined; + + const value = this.cache.get(key); + this.cache.delete(key); + this.cache.set(key, value); + return value; + } + + /** + * Set key-value pair + * @param {*} key - Cache key + * @param {*} value - Value to store + */ + put(key, value) { + if (this.cache.has(key)) { + this.cache.delete(key); + } + + this.cache.set(key, value); + if (this.cache.size > this.capacity) { + this.cache.delete(this.cache.keys().next().value); + } + } + + /** + * Check if key exists (without updating recency) + * @param {*} key - Cache key + * @returns {boolean} + */ + has(key) { + return this.cache.has(key); + } + + /** + * Delete a key + * @param {*} key - Cache key + * @returns {boolean} true if key existed + */ + delete(key) { + const itemExist = this.has(key); + if (itemExist) + this.cache.delete(key); + return itemExist; + } + + /** + * Clear all items + */ + clear() { + this.cache.clear(); + } + + /** + * Current number of items + * @returns {number} + */ + get size() { + return this.cache.size; + } + + /** + * Get all keys in order (least recent first) + * @returns {Array} Array of keys + */ + keys() { + return Array.of(...this.cache.keys()); + } + + /** + * Get all values in order (least recent first) + * @returns {Array} Array of values + */ + values() { + return Array.of(...this.cache.values()); + } } -module.exports = { LRUCache }; +module.exports = {LRUCache}; diff --git a/11-singleton/index.js b/11-singleton/index.js index 3c98305..09b8256 100644 --- a/11-singleton/index.js +++ b/11-singleton/index.js @@ -8,32 +8,24 @@ * 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; - - // 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"); - } - - // Step 3: Optionally prevent direct instantiation - // constructor() { - // if (Singleton.instance) { - // throw new Error('Use Singleton.getInstance()'); - // } - // } - - // Step 4: Add a reset method for testing - static resetInstance() { - // TODO: Reset the instance to null - } + static instance = null; + + static getInstance() { + if (!this.instance) + this.instance = new this; + + return this.instance; + } + + constructor() { + if (Singleton.instance) { + throw new Error('Use Singleton.getInstance()'); + } + } + + static resetInstance() { + this.instance = null; + } } /** @@ -45,28 +37,19 @@ 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; - - // Step 2: Return an object with getInstance method - // getInstance should: - // - Accept arguments to pass to constructor - // - Only create instance on first call - // - Return the same instance on subsequent calls - - // Step 3: Optionally add resetInstance method - - return { - getInstance: (...args) => { - // TODO: Implement - throw new Error("Not implemented"); - }, - resetInstance: () => { - // TODO: Implement - }, - }; + let instance = null; + + return { + getInstance: (...args) => { + if (!instance) + instance = new Class(...args); + + return instance; + }, + resetInstance: () => { + instance = null; + }, + }; } /** @@ -75,30 +58,30 @@ function createSingleton(Class) { * A practical example of a singleton for database connections. */ class DatabaseConnection { - constructor(connectionString) { - this.connectionString = connectionString; - this.connected = false; - } - - connect() { - if (!this.connected) { - // Simulate connection - this.connected = true; - console.log(`Connected to ${this.connectionString}`); + constructor(connectionString) { + this.connectionString = connectionString; + this.connected = false; + } + + connect() { + if (!this.connected) { + // Simulate connection + this.connected = true; + console.log(`Connected to ${this.connectionString}`); + } + return this; } - return this; - } - query(sql) { - if (!this.connected) { - throw new Error("Not connected"); + query(sql) { + if (!this.connected) { + throw new Error("Not connected"); + } + return `Executing: ${sql}`; } - return `Executing: ${sql}`; - } - disconnect() { - this.connected = false; - } + disconnect() { + this.connected = false; + } } /** @@ -107,27 +90,27 @@ class DatabaseConnection { * A practical example of a singleton for app configuration. */ class AppConfig { - constructor() { - this.settings = {}; - } - - set(key, value) { - this.settings[key] = value; - return this; - } - - get(key) { - return this.settings[key]; - } - - getAll() { - return { ...this.settings }; - } + constructor() { + this.settings = {}; + } + + set(key, value) { + this.settings[key] = value; + return this; + } + + get(key) { + return this.settings[key]; + } + + getAll() { + return {...this.settings}; + } } module.exports = { - Singleton, - createSingleton, - DatabaseConnection, - AppConfig, + Singleton, + createSingleton, + DatabaseConnection, + AppConfig, }; diff --git a/12-factory-pattern/index.js b/12-factory-pattern/index.js index 3e0bd14..74f0dfb 100644 --- a/12-factory-pattern/index.js +++ b/12-factory-pattern/index.js @@ -4,55 +4,53 @@ // Example shape classes for the factory class Circle { - constructor({ radius }) { - this.radius = radius; - this.type = "circle"; - } - - area() { - return Math.PI * this.radius ** 2; - } - - perimeter() { - return 2 * Math.PI * this.radius; - } + constructor({radius}) { + this.radius = radius; + this.type = "circle"; + } + + area() { + return Math.PI * this.radius ** 2; + } + + perimeter() { + return 2 * Math.PI * this.radius; + } } class Rectangle { - constructor({ width, height }) { - this.width = width; - this.height = height; - this.type = "rectangle"; - } - - area() { - return this.width * this.height; - } - - perimeter() { - return 2 * (this.width + this.height); - } + constructor({width, height}) { + this.width = width; + this.height = height; + this.type = "rectangle"; + } + + area() { + return this.width * this.height; + } + + perimeter() { + return 2 * (this.width + this.height); + } } class Triangle { - constructor({ - base, - height, - sides = [base, height, Math.sqrt(base ** 2 + height ** 2)], - }) { - this.base = base; - this.height = height; - this.sides = sides; - this.type = "triangle"; - } - - area() { - return (this.base * this.height) / 2; - } - - perimeter() { - return this.sides.reduce((sum, side) => sum + side, 0); - } + constructor({ + base, height, sides = [base, height, Math.sqrt(base ** 2 + height ** 2)], + }) { + this.base = base; + this.height = height; + this.sides = sides; + this.type = "triangle"; + } + + area() { + return (this.base * this.height) / 2; + } + + perimeter() { + return this.sides.reduce((sum, side) => sum + side, 0); + } } /** @@ -61,22 +59,24 @@ 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') - * @param {Object} options - Shape options - * @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 - }, + /** + * Create a shape instance + * @param {string} type - Shape type ('circle', 'rectangle', 'triangle') + * @param {Object} options - Shape options + * @returns {Object} Shape instance + */ + create(type, options) { + switch (type) { + case 'circle': + return new Circle(options); + case 'rectangle': + return new Rectangle(options); + case 'triangle': + return new Triangle(options); + default: + throw Error("Unknown type"); + } + }, }; /** @@ -85,85 +85,84 @@ const ShapeFactory = { * A factory class where types can be registered dynamically. */ class Factory { - constructor() { - // TODO: Initialize registry - // this.registry = new Map(); - } - - /** - * Register a type with the factory - * @param {string} type - Type name - * @param {Function} Class - Constructor function - * @param {Object} [options] - Registration options - * @param {string[]} [options.required] - Required argument keys - * @param {Function} [options.validate] - Validation function - */ - register(type, Class, options = {}) { - // TODO: Implement register - // Store the class and options in the registry - } - - /** - * Unregister a type - * @param {string} type - Type name - * @returns {boolean} true if type was registered - */ - unregister(type) { - // TODO: Implement unregister - - throw new Error("Not implemented"); - } - - /** - * Create an instance of a registered type - * @param {string} type - Type name - * @param {Object} args - Constructor arguments - * @returns {Object} Instance of the type - */ - create(type, args = {}) { - // TODO: Implement create - - // Step 1: Check if type is registered - - // Step 2: Get the class and options - - // Step 3: Validate required fields (if specified) - - // Step 4: Run custom validation (if specified) - - // Step 5: Create and return instance - - return null; // Replace with implementation - } - - /** - * Check if a type is registered - * @param {string} type - Type name - * @returns {boolean} - */ - has(type) { - // TODO: Implement has - - throw new Error("Not implemented"); - } - - /** - * Get all registered type names - * @returns {string[]} - */ - getTypes() { - // TODO: Implement getTypes - - throw new Error("Not implemented"); - } - - /** - * Clear all registered types - */ - clear() { - // TODO: Implement clear - throw new Error("Not implemented"); - } + constructor() { + this.registry = new Map(); + } + + /** + * Register a type with the factory + * @param {string} type - Type name + * @param {Function} Class - Constructor function + * @param {Object} [options] - Registration options + * @param {string[]} [options.required] - Required argument keys + * @param {Function} [options.validate] - Validation function + */ + register(type, Class, options = {}) { + this.registry.set(type, {Class: Class, options: options}); + } + + /** + * Unregister a type + * @param {string} type - Type name + * @returns {boolean} true if type was registered + */ + unregister(type) { + const registered = this.registry.has(type); + if (registered) this.registry.delete(type); + + return registered; + } + + /** + * Create an instance of a registered type + * @param {string} type - Type name + * @param {Object} args - Constructor arguments + * @returns {Object} Instance of the type + */ + create(type, args = {}) { + if (!this.registry.has(type)) throw Error("Unregistered"); + + const {Class, options} = this.registry.get(type); + if (args === {}) throw Error("Empty"); + + if (options.hasOwnProperty("required")) { + const required = options.required; + required.forEach(item => { + if (!args.hasOwnProperty(item)) throw Error(`Required property: ${item} no exist`); + }); + } + + if (options.hasOwnProperty("validate")) { + const validation = options.validate; + if (!validation(args)) throw Error(`Validation failed`); + } + + return new Class({...args, ...options}); + } + + /** + * Check if a type is registered + * @param {string} type - Type name + * @returns {boolean} + */ + has(type) { + return this.registry.has(type); + } + + /** + * Get all registered type names + * @returns {string[]} + */ + getTypes() { + return Array.of(...this.registry.keys()); + } + + /** + * Clear all registered types + */ + clear() { + this.registry.clear(); + } } /** @@ -172,73 +171,66 @@ class Factory { * Practical example of factory for different logger types. */ class ConsoleLogger { - constructor({ prefix = "" } = {}) { - this.prefix = prefix; - } + constructor({prefix = ""} = {}) { + this.prefix = prefix; + } - log(message) { - console.log(`${this.prefix}${message}`); - } + log(message) { + console.log(`${this.prefix}${message}`); + } - error(message) { - console.error(`${this.prefix}ERROR: ${message}`); - } + error(message) { + console.error(`${this.prefix}ERROR: ${message}`); + } } class FileLogger { - constructor({ path, prefix = "" } = {}) { - this.path = path; - this.prefix = prefix; - this.logs = []; // Simulated file - } - - log(message) { - this.logs.push(`${this.prefix}${message}`); - } - - error(message) { - this.logs.push(`${this.prefix}ERROR: ${message}`); - } - - getLogs() { - return [...this.logs]; - } + constructor({path, prefix = ""} = {}) { + this.path = path; + this.prefix = prefix; + this.logs = []; // Simulated file + } + + log(message) { + this.logs.push(`${this.prefix}${message}`); + } + + error(message) { + this.logs.push(`${this.prefix}ERROR: ${message}`); + } + + getLogs() { + return [...this.logs]; + } } class JsonLogger { - constructor({ includeTimestamp = true } = {}) { - this.includeTimestamp = includeTimestamp; - this.entries = []; - } - - log(message) { - this.entries.push(this._createEntry("info", message)); - } - - error(message) { - this.entries.push(this._createEntry("error", message)); - } - - _createEntry(level, message) { - const entry = { level, message }; - if (this.includeTimestamp) { - entry.timestamp = new Date().toISOString(); - } - return entry; - } - - getEntries() { - return [...this.entries]; - } + constructor({includeTimestamp = true} = {}) { + this.includeTimestamp = includeTimestamp; + this.entries = []; + } + + log(message) { + this.entries.push(this._createEntry("info", message)); + } + + error(message) { + this.entries.push(this._createEntry("error", message)); + } + + _createEntry(level, message) { + const entry = {level, message}; + if (this.includeTimestamp) { + entry.timestamp = new Date().toISOString(); + } + return entry; + } + + getEntries() { + return [...this.entries]; + } } module.exports = { - ShapeFactory, - Factory, - Circle, - Rectangle, - Triangle, - ConsoleLogger, - FileLogger, - JsonLogger, + ShapeFactory, Factory, Circle, Rectangle, Triangle, ConsoleLogger, FileLogger, JsonLogger, }; diff --git a/13-decorator-pattern/index.js b/13-decorator-pattern/index.js index 6a3646f..4c461d8 100644 --- a/13-decorator-pattern/index.js +++ b/13-decorator-pattern/index.js @@ -11,22 +11,14 @@ * @returns {Function} Decorated function */ function withLogging(fn) { - // TODO: Implement withLogging + return function (...args) { + const value = fn.call(this, ...args); - // Step 1: Return a new function that wraps fn + console.log(`name: ${fn.name}, args: ${args}`); + console.log(`value: ${value}`); - // Step 2: Log the function name and arguments - - // Step 3: Call the original function - - // Step 4: Log the return value - - // Step 5: Return the result - - // Note: Preserve 'this' context using apply/call - - // Broken: throws error - throw new Error("Not implemented"); + return value; + }; } /** @@ -38,19 +30,14 @@ function withLogging(fn) { * @returns {Function} Decorated function */ function withTiming(fn) { - // TODO: Implement withTiming - - // Step 1: Return a new function - - // Step 2: Record start time (performance.now() or Date.now()) + return function (...args) { + const startTime = Date.now(); + const result = fn.call(this, ...args); - // Step 3: Call original function + console.log(`Duration: ${Date.now() - startTime}`); - // Step 4: Calculate and log duration - - // Step 5: Return result - - return () => undefined; // Broken placeholder + return result; + }; } /** @@ -63,20 +50,19 @@ 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) { + let lastError; + + for (let i = 0; i < maxRetries + 1; i++) { + try { + return fn.call(this, ...args); + } catch (e) { + lastError = e; + } + } + + throw lastError; + }; } /** @@ -88,11 +74,17 @@ function withRetry(fn, maxRetries = 3) { * @returns {Function} Decorated function with cache */ function withMemoize(fn) { - // TODO: Implement withMemoize + const cache = new Map(); + + return function (...args) { + const key = JSON.stringify(args); - // Similar to memoization assignment but as a decorator + if (cache.has(key)) return cache.get(key); - return () => undefined; // Broken placeholder + const result = fn.call(this, ...args); + cache.set(key, result); + return result; + }; } /** @@ -105,17 +97,11 @@ 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 + return function (...args) { + if (!validator(...args)) throw Error('Validation fails'); - // Step 3: If validation fails, throw error - - // Step 4: If passes, call original function - - return () => undefined; // Broken placeholder + return fn.call(this, ...args); + }; } /** @@ -128,19 +114,18 @@ function withValidation(fn, validator) { * @returns {Object} Object with cached method */ function withCache(obj, methodName) { - // TODO: Implement withCache - - // Step 1: Get the original method + const original = obj[methodName]; - // Step 2: Create a cache (Map) + const cache = new Map(); + obj[methodName] = function (...args) { + const key = JSON.stringify(args); - // Step 3: Replace the method with a caching wrapper + if (cache.has(key)) return cache.get(key); - // Step 4: Return the object - - // Broken: deletes the method instead of caching it - delete obj[methodName]; - return obj; + const value = original.call(this, ...args); + cache.set(key, value); + return value; + }; } /** @@ -153,15 +138,14 @@ 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 (fn) => { + let result = fn; + for (let i = decorators.length - 1; i >= 0; i--) { + result = decorators[i](result); + } + + return result; + }; } /** @@ -173,41 +157,42 @@ 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 (fn) => { + let result = fn; + for (let i = 0; i < decorators.length; i++) { + result = decorators[i](result); + } + + return result; + }; } // Storage for logs (used in tests) const logs = []; function log(message) { - logs.push(message); - // console.log(message); // Uncomment for debugging + logs.push(message); + // console.log(message); // Uncomment for debugging } function clearLogs() { - logs.length = 0; + logs.length = 0; } function getLogs() { - return [...logs]; + return [...logs]; } module.exports = { - withLogging, - withTiming, - withRetry, - withMemoize, - withValidation, - withCache, - compose, - pipe, - log, - clearLogs, - getLogs, + withLogging, + withTiming, + withRetry, + withMemoize, + withValidation, + withCache, + compose, + pipe, + log, + clearLogs, + getLogs, }; diff --git a/14-middleware-pipeline/index.js b/14-middleware-pipeline/index.js index 8db7620..e2dde55 100644 --- a/14-middleware-pipeline/index.js +++ b/14-middleware-pipeline/index.js @@ -1,65 +1,53 @@ +const {run} = require("jest"); + /** * Middleware Pipeline Implementation * * An Express/Koa-style middleware pipeline. */ class Pipeline { - constructor() { - // TODO: Initialize middleware array - // this.middleware = []; - } - - /** - * Add middleware to the pipeline - * @param {Function} fn - Middleware function (ctx, next) => {} - * @returns {Pipeline} this (for chaining) - */ - use(fn) { - // TODO: Implement use - - // Step 1: Validate fn is a function - - // Step 2: Add to middleware array - - // Step 3: Return this for chaining - - return null; // Broken: should return this - } - - /** - * Execute the pipeline with given context - * @param {Object} context - Context object passed to all middleware - * @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) - - // Step 2: Start dispatch at index 0 - - // Step 3: Return promise for async support - - // Broken: rejects instead of resolving - return Promise.reject(new Error("Not implemented")); - } - - /** - * Compose middleware into a single function - * @returns {Function} Composed middleware function - */ - compose() { - // TODO: Implement compose - - // Return a function that takes context and runs the pipeline - - return (context) => this.run(context); - } + constructor() { + this.middleware = []; + } + + /** + * Add middleware to the pipeline + * @param {Function} fn - Middleware function (ctx, next) => {} + * @returns {Pipeline} this (for chaining) + */ + use(fn) { + if (typeof fn !== 'function') return this; + + this.middleware.push(fn); + return this; + } + + /** + * Execute the pipeline with given context + * @param {Object} context - Context object passed to all middleware + * @returns {Promise} Resolves when pipeline completes + */ + run(context) { + const pipeline = this; + + function dispatch(index) { + const middleware = pipeline.middleware[index]; + + if (!middleware) return Promise.resolve(); + + return Promise.resolve(middleware(context, () => dispatch(index + 1))); + } + + return dispatch(0); + } + + /** + * Compose middleware into a single function + * @returns {Function} Composed middleware function + */ + compose() { + return (context) => this.run(context); + } } /** @@ -71,31 +59,19 @@ class Pipeline { * @returns {Function} Composed function (context) => Promise */ function compose(middleware) { - // TODO: Implement compose - - // Validate all items are functions - - // Return a function that: - // - Takes context - // - Creates dispatch(index) that calls middleware[index] - // - Returns dispatch(0) - - return function (context) { - function dispatch(index) { - // TODO: Implement dispatch + if (!middleware.every(m => typeof m === 'function')) throw Error(); - // Step 1: Get middleware at index - // Step 2: If none, return resolved promise - // Step 3: Create next function = () => dispatch(index + 1) - // Step 4: Call middleware with (context, next) - // Step 5: Return as promise + return function (context) { + function dispatch(index) { + const middlewareElement = middleware[index]; + if (!middlewareElement) return Promise.resolve(); - // Broken: rejects instead of resolving - return Promise.reject(new Error("Not implemented")); - } + const next = () => dispatch(index + 1); + return Promise.resolve(middlewareElement(context, next)); + } - return dispatch(0); - }; + return dispatch(0); + }; } /** @@ -106,16 +82,11 @@ 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"); - }; + return (ctx, next) => { + if (condition(ctx)) + middleware(ctx, next); + next.call(); + }; } /** @@ -125,20 +96,18 @@ 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"); - }; + return async (ctx, next) => { + try { + await next(); + } catch (e) { + errorHandler(e, ctx); + } + }; } module.exports = { - Pipeline, - compose, - when, - errorMiddleware, + Pipeline, + compose, + when, + errorMiddleware, }; diff --git a/15-dependency-injection/index.js b/15-dependency-injection/index.js index e4995b5..b6cd523 100644 --- a/15-dependency-injection/index.js +++ b/15-dependency-injection/index.js @@ -2,120 +2,116 @@ * Dependency Injection Container Implementation */ class Container { - constructor() { - // TODO: Initialize registry - // this.registry = new Map(); - } - - /** - * Register a class with the container - * @param {string} name - Service name - * @param {Function} Class - Constructor function - * @param {string[]} [dependencies=[]] - Names of dependencies - * @param {Object} [options={}] - Registration options - * @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 } - } - - /** - * Register an existing instance - * @param {string} name - Service name - * @param {*} instance - Instance to register - */ - registerInstance(name, instance) { - // TODO: Implement registerInstance - // Store in registry: - // { type: 'instance', instance } - } - - /** - * Register a factory function - * @param {string} name - Service name - * @param {Function} factory - Factory function - * @param {string[]} [dependencies=[]] - Names of dependencies - * @param {Object} [options={}] - Registration options - */ - registerFactory(name, factory, dependencies = [], options = {}) { - // TODO: Implement registerFactory - // Store in registry: - // { type: 'factory', factory, dependencies, singleton, instance: null } - } - - /** - * Resolve a service by name - * @param {string} name - Service name - * @param {Set} [resolutionStack] - Stack for circular dependency detection - * @returns {*} The resolved instance - */ - resolve(name, resolutionStack = new Set()) { - // TODO: Implement resolve - - // Step 1: Check if service is registered - // Throw error if not found - - // Step 2: Check for circular dependencies - // If name is already in resolutionStack, throw error - - // Step 3: Get registration from registry - - // Step 4: Handle different types: - - // For 'instance': - // - Return the stored instance - - // For 'class' or 'factory': - // - If singleton and instance exists, return instance - // - Add name to resolutionStack - // - Resolve all dependencies recursively - // - Create instance (new Class(...deps) or factory(...deps)) - // - Remove name from resolutionStack - // - If singleton, cache instance - // - Return instance - - // Broken: returns undefined (causes test assertions to fail) - return undefined; - } - - /** - * Check if a service is registered - * @param {string} name - Service name - * @returns {boolean} - */ - has(name) { - // TODO: Implement has - throw new Error("Not implemented"); - } - - /** - * Unregister a service - * @param {string} name - Service name - * @returns {boolean} true if was registered - */ - unregister(name) { - // TODO: Implement unregister - throw new Error("Not implemented"); - } - - /** - * Clear all registrations - */ - clear() { - // TODO: Implement clear - throw new Error("Not implemented"); - } - - /** - * Get all registered service names - * @returns {string[]} - */ - getRegistrations() { - // TODO: Implement getRegistrations - throw new Error("Not implemented"); - } + constructor() { + this.registry = new Map(); + } + + /** + * Register a class with the container + * @param {string} name - Service name + * @param {Function} Class - Constructor function + * @param {string[]} [dependencies=[]] - Names of dependencies + * @param {Object} [options={}] - Registration options + * @param {boolean} [options.singleton=false] - Whether to create singleton + */ + register(name, Class, dependencies = [], options = {}) { + this.registry.set(name, {type: 'class', Class, dependencies, options, instance: null}); + } + + /** + * Register an existing instance + * @param {string} name - Service name + * @param {*} instance - Instance to register + */ + registerInstance(name, instance) { + this.registry.set(name, {type: 'instance', instance}); + } + + /** + * Register a factory function + * @param {string} name - Service name + * @param {Function} factory - Factory function + * @param {string[]} [dependencies=[]] - Names of dependencies + * @param {Object} [options={}] - Registration options + */ + registerFactory(name, factory, dependencies = [], options = {}) { + this.registry.set(name, {type: 'factory', factory, dependencies, options, instance: null}); + } + + /** + * Resolve a service by name + * @param {string} name - Service name + * @param {Set} [resolutionStack] - Stack for circular dependency detection + * @returns {*} The resolved instance + */ + resolve(name, resolutionStack = new Set()) { + if (!this.registry.has(name)) throw Error(); + + if (resolutionStack.has(name)) throw Error('circular'); + + const registration = this.registry.get(name); + + switch (registration.type) { + case 'instance': + return registration.instance; + case 'class': + case 'factory': + const isSingleton = registration.options["singleton"] || false; + + if (isSingleton && registration.instance) { + return registration.instance; + } + + resolutionStack.add(name); + const dependencies = registration.dependencies.map(dep => this.resolve(dep, resolutionStack)); + let instance = registration.type === 'class' + ? new registration.Class(...dependencies) + : registration.factory(...dependencies); + + resolutionStack.delete(name); + if (isSingleton) registration.instance = instance; + + return instance; + } + } + + /** + * Check if a service is registered + * @param {string} name - Service name + * @returns {boolean} + */ + has(name) { + return this.registry.has(name); + } + + /** + * Unregister a service + * @param {string} name - Service name + * @returns {boolean} true if was registered + */ + unregister(name) { + const exist = this.has(name); + + if (exist) + return this.registry.delete(name); + + return exist; + } + + /** + * Clear all registrations + */ + clear() { + this.registry.clear(); + } + + /** + * Get all registered service names + * @returns {string[]} + */ + getRegistrations() { + return Array.of(...this.registry.keys()); + } } /** @@ -125,78 +121,82 @@ class Container { * @returns {Container} Child container */ function createChildContainer(parent) { - // TODO: Implement createChildContainer + const child = new Container(); - // Create a new container that: - // - First checks its own registry - // - Falls back to parent for unregistered services + const originalResolve = child.resolve.bind(child); - const child = new Container(); - // Override resolve to check parent... - return child; + child.has = function (name) { + return this.registry.has(name) || parent.has(name); + }; + + child.resolve = function (name, resolutionStack = new Set()) { + if (this.registry.has(name)) + return originalResolve(name, resolutionStack); + + return parent.resolve(name, resolutionStack); + }; + + return child; } // Example classes for testing class Logger { - constructor() { - this.logs = []; - } + constructor() { + this.logs = []; + } - log(message) { - this.logs.push(message); - } + log(message) { + this.logs.push(message); + } - getLogs() { - return [...this.logs]; - } + getLogs() { + return [...this.logs]; + } } class Database { - constructor(logger) { - this.logger = logger; - this.connected = false; - } - - connect() { - this.logger.log("Database connected"); - this.connected = true; - } - - query(sql) { - this.logger.log(`Query: ${sql}`); - return []; - } + constructor(logger) { + this.logger = logger; + this.connected = false; + } + + connect() { + this.logger.log("Database connected"); + this.connected = true; + } + + query(sql) { + this.logger.log(`Query: ${sql}`); + return []; + } } class UserRepository { - constructor(database, logger) { - this.database = database; - this.logger = logger; - } - - findById(id) { - this.logger.log(`Finding user ${id}`); - return this.database.query(`SELECT * FROM users WHERE id = ${id}`); - } + constructor(database, logger) { + this.database = database; + this.logger = logger; + } + + findById(id) { + this.logger.log(`Finding user ${id}`); + return this.database.query(`SELECT * + FROM users + WHERE id = ${id}`); + } } class UserService { - constructor(userRepository, logger) { - this.userRepository = userRepository; - this.logger = logger; - } - - getUser(id) { - this.logger.log(`Getting user ${id}`); - return this.userRepository.findById(id); - } + constructor(userRepository, logger) { + this.userRepository = userRepository; + this.logger = logger; + } + + getUser(id) { + this.logger.log(`Getting user ${id}`); + return this.userRepository.findById(id); + } } module.exports = { - Container, - createChildContainer, - Logger, - Database, - UserRepository, - UserService, + Container, createChildContainer, Logger, Database, UserRepository, UserService, }; diff --git a/16-state-machine/index.js b/16-state-machine/index.js index d0bad18..67d881f 100644 --- a/16-state-machine/index.js +++ b/16-state-machine/index.js @@ -2,124 +2,142 @@ * State Machine Implementation */ class StateMachine { - /** - * Create a state machine - * @param {Object} config - Machine configuration - * @param {string} config.initial - Initial state - * @param {Object} config.states - State definitions - * @param {Object} [config.context] - Initial context data - */ - constructor(config) { - // TODO: Implement constructor - // Step 1: Validate config has initial and states - // Step 2: Store configuration - // this.config = config; - // this.currentState = config.initial; - // this.context = config.context || {}; - // Step 3: Validate initial state exists in states - } - - /** - * Get current state - * @returns {string} - */ - get state() { - // TODO: Return current state - throw new Error("Not implemented"); - } - - /** - * Attempt a state transition - * @param {string} event - Event name - * @param {Object} [payload] - Optional data for the transition - * @returns {boolean} Whether transition was successful - */ - transition(event, payload) { - // TODO: Implement transition - - // Step 1: Get current state config - - // Step 2: Check if event is valid for current state - // Return false if not - - // 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 - - // Step 7: Return true - - throw new Error("Not implemented"); - } - - /** - * Check if a transition is possible - * @param {string} event - Event name - * @returns {boolean} - */ - can(event) { - // TODO: Implement can - - // Check if event exists for current state - // Check guard if present - - throw new Error("Not implemented"); - } - - /** - * Get available transitions from current state - * @returns {string[]} Array of event names - */ - getAvailableTransitions() { - // TODO: Implement getAvailableTransitions - - // Return array of event names from current state's 'on' config - - throw new Error("Not implemented"); - } - - /** - * Get the context data - * @returns {Object} - */ - getContext() { - // TODO: Return context - throw new Error("Not implemented"); - } - - /** - * Update context data - * @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 - } - - /** - * Check if machine is in a final state (no transitions out) - * @returns {boolean} - */ - isFinal() { - // TODO: Check if current state has no transitions - throw new Error("Not implemented"); - } - - /** - * Reset machine to initial state - * @param {Object} [newContext] - Optional new context - */ - reset(newContext) { - // TODO: Reset to initial state - // Optionally reset context - } + /** + * Create a state machine + * @param {Object} config - Machine configuration + * @param {string} config.initial - Initial state + * @param {Object} config.states - State definitions + * @param {Object} [config.context] - Initial context data + */ + constructor(config) { + if (!config) throw new Error() + if (!config.initial) throw new Error() + if (!config.states) throw new Error() + if (!config.states[config.initial]) throw new Error(); + + this.config = config; + this.currentState = config.initial; + this.context = config.context || {}; + + } + + /** + * Get current state + * @returns {string} + */ + get state() { + return this.currentState; + } + + /** + * Attempt a state transition + * @param {string} event - Event name + * @param {Object} [payload] - Optional data for the transition + * @returns {boolean} Whether transition was successful + */ + transition(event, payload) { + const currentStateConfig = this.config.states[this.currentState]; + const transition = currentStateConfig.on; + + if (!transition) return false; + if (!transition[event]) return false; + + const rule = transition[event]; + + let target, guard, action; + + if (typeof rule === 'string') + target = rule; + else { + target = rule.target; + guard = rule.guard; + action = rule.action; + } + + if (guard) { + if (!guard(this.context, payload)) return false; + } + + this.currentState = target; + + if (action) action(this.context, payload) + + return true; + } + + /** + * Check if a transition is possible + * @param {string} event - Event name + * @returns {boolean} + */ + can(event) { + const currentStateConfig = this.config.states[this.currentState]; + const transition = currentStateConfig.on; + + if (!transition) return false; + if (!transition[event]) return false; + + const rule = transition[event]; + + if (typeof rule === 'string') return true; + + const guard = rule.guard; + + if (guard) return true; + + return guard(this.context, {}) + } + + /** + * Get available transitions from current state + * @returns {string[]} Array of event names + */ + getAvailableTransitions() { + const currentContextState = this.config.states[this.currentState]; + + if (!currentContextState.on) return [] + + return Object.keys(currentContextState.on) + } + + /** + * Get the context data + * @returns {Object} + */ + getContext() { + return this.context; + } + + /** + * Update context data + * @param {Object|Function} updater - New context or updater function + */ + updateContext(updater) { + if (typeof updater === "function") { + this.context = updater(this.context); + return; + } + + this.context = {...this.context, ...updater}; + } + + /** + * Check if machine is in a final state (no transitions out) + * @returns {boolean} + */ + isFinal() { + const currentSateConfig = this.config.states[this.currentState]; + return (!currentSateConfig.on || Object.keys(currentSateConfig.on).length === 0) + } + + /** + * Reset machine to initial state + * @param {Object} [newContext] - Optional new context + */ + reset(newContext) { + this.context = newContext; + this.currentState = this.config.initial; + } } /** @@ -129,12 +147,7 @@ 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 - - return () => new StateMachine(config); + return () => new StateMachine(config); } -module.exports = { StateMachine, createMachine }; +module.exports = {StateMachine, createMachine}; diff --git a/17-command-pattern/index.js b/17-command-pattern/index.js index e8cd005..6cf616f 100644 --- a/17-command-pattern/index.js +++ b/17-command-pattern/index.js @@ -8,172 +8,158 @@ * Manages command execution with undo/redo support. */ class CommandManager { - constructor() { - // TODO: Initialize stacks - // this.undoStack = []; - // this.redoStack = []; - } - - /** - * Execute a command - * @param {Object} command - Command with execute() method - */ - execute(command) { - // TODO: Implement execute - // Step 1: Call command.execute() - // Step 2: Push to undo stack - // Step 3: Clear redo stack (new action invalidates redo history) - } - - /** - * Undo the last command - * @returns {boolean} Whether undo was performed - */ - undo() { - // TODO: Implement undo - - // Step 1: Check if undo stack is empty - - // Step 2: Pop command from undo stack - - // Step 3: Call command.undo() - - // Step 4: Push to redo stack - - // Step 5: Return true - - throw new Error("Not implemented"); - } - - /** - * Redo the last undone command - * @returns {boolean} Whether redo was performed - */ - redo() { - // TODO: Implement redo - - // Step 1: Check if redo stack is empty - - // Step 2: Pop command from redo stack - - // Step 3: Call command.execute() - - // Step 4: Push to undo stack - - // Step 5: Return true - - throw new Error("Not implemented"); - } - - /** - * Check if undo is available - * @returns {boolean} - */ - canUndo() { - // TODO: Return whether undo stack has items - throw new Error("Not implemented"); - } - - /** - * Check if redo is available - * @returns {boolean} - */ - canRedo() { - // TODO: Return whether redo stack has items - throw new Error("Not implemented"); - } - - /** - * Get command history (executed commands) - * @returns {Object[]} - */ - get history() { - // TODO: Return copy of undo stack - throw new Error("Not implemented"); - } - - /** - * Clear all history - */ - clear() { - // TODO: Clear both stacks - } + constructor() { + this.undoStack = []; + this.redoStack = []; + } + + /** + * Execute a command + * @param {Object} command - Command with execute() method + */ + execute(command) { + command.execute() + this.undoStack.push(command); + this.redoStack = []; + } + + /** + * Undo the last command + * @returns {boolean} Whether undo was performed + */ + undo() { + if (this.undoStack.length === 0) return false; + + const command = this.undoStack.pop(); + command.undo(); + this.redoStack.push(command); + + return true; + } + + /** + * Redo the last undone command + * @returns {boolean} Whether redo was performed + */ + redo() { + if (this.redoStack.length === 0) return false; + + const command = this.redoStack.pop(); + command.execute(); + this.undoStack.push(command); + + return true; + } + + /** + * Check if undo is available + * @returns {boolean} + */ + canUndo() { + return this.undoStack.length !== 0; + } + + /** + * Check if redo is available + * @returns {boolean} + */ + canRedo() { + return this.redoStack.length !== 0; + } + + /** + * Get command history (executed commands) + * @returns {Object[]} + */ + get history() { + return Array.of(...this.undoStack); + } + + /** + * Clear all history + */ + clear() { + this.undoStack = []; + this.redoStack = []; + } } /** * Add Command */ class AddCommand { - constructor(calculator, value) { - // TODO: Store calculator and value - // this.calculator = calculator; - // this.value = value; - this.description = `Add ${value}`; - } - - execute() { - // TODO: Add value to calculator.value - } - - undo() { - // TODO: Subtract value from calculator.value - } + constructor(calculator, value) { + this.calculator = calculator; + this.value = value; + this.description = `Add ${value}`; + } + + execute() { + this.calculator.value += this.value; + } + + undo() { + this.calculator.value -= this.value; + } } /** * Subtract Command */ class SubtractCommand { - constructor(calculator, value) { - // TODO: Store calculator and value - this.description = `Subtract ${value}`; - } - - execute() { - // TODO: Subtract value from calculator.value - } - - undo() { - // TODO: Add value to calculator.value - } + constructor(calculator, value) { + this.calculator = calculator; + this.value = value; + this.description = `Subtract ${value}`; + } + + execute() { + this.calculator.value -= this.value; + } + + undo() { + this.calculator.value += this.value; + } } /** * Multiply Command */ class MultiplyCommand { - constructor(calculator, value) { - // TODO: Store calculator, value, and previous value for undo - this.description = `Multiply by ${value}`; - } - - execute() { - // TODO: Multiply calculator.value by value - // Save previous value for undo - } - - undo() { - // TODO: Restore previous value - } + constructor(calculator, value) { + this.calculator = calculator; + this.value = value; + this.description = `Multiply by ${value}`; + } + + execute() { + this.previousValue = this.calculator.value; + this.calculator.value *= this.value; + } + + undo() { + this.calculator.value = this.previousValue; + } } /** * Divide Command */ class DivideCommand { - constructor(calculator, value) { - // TODO: Store calculator, value, and previous value for undo - this.description = `Divide by ${value}`; - } - - execute() { - // TODO: Divide calculator.value by value - // Save previous value for undo - } - - undo() { - // TODO: Restore previous value - } + constructor(calculator, value) { + this.calculator = calculator; + this.value = value; + this.description = `Divide by ${value}`; + } + + execute() { + this.previousValue = this.calculator.value; + this.calculator.value /= this.value; + } + + undo() { + this.calculator.value = this.previousValue; + } } /** @@ -182,27 +168,26 @@ class DivideCommand { * Groups multiple commands into one. */ class MacroCommand { - constructor(commands = []) { - // TODO: Store commands array - // this.commands = commands; - this.description = "Macro"; - } - - /** - * Add a command to the macro - * @param {Object} command - */ - add(command) { - // TODO: Add command to array - } - - execute() { - // TODO: Execute all commands in order - } - - undo() { - // TODO: Undo all commands in reverse order - } + constructor(commands = []) { + this.commands = commands; + this.description = "Macro"; + } + + /** + * Add a command to the macro + * @param {Object} command + */ + add(command) { + this.commands.push(command); + } + + execute() { + this.commands.map(command => command.execute()); + } + + undo() { + this.commands.toReversed().map(command => command.undo()); + } } /** @@ -211,26 +196,28 @@ class MacroCommand { * Sets calculator to specific value (useful for testing). */ class SetValueCommand { - constructor(calculator, value) { - // TODO: Store calculator, new value, and previous value - this.description = `Set to ${value}`; - } - - execute() { - // TODO: Save previous, set new value - } - - undo() { - // TODO: Restore previous value - } + constructor(calculator, value) { + this.calculator = calculator; + this.value = value; + this.description = `Set to ${value}`; + } + + execute() { + this.previousValue = this.calculator.value; + this.calculator.value = this.value; + } + + undo() { + this.calculator.value = this.previousValue; + } } module.exports = { - CommandManager, - AddCommand, - SubtractCommand, - MultiplyCommand, - DivideCommand, - MacroCommand, - SetValueCommand, + CommandManager, + AddCommand, + SubtractCommand, + MultiplyCommand, + DivideCommand, + MacroCommand, + SetValueCommand, }; diff --git a/18-strategy-pattern/index.js b/18-strategy-pattern/index.js index 807bcd2..5e0e29a 100644 --- a/18-strategy-pattern/index.js +++ b/18-strategy-pattern/index.js @@ -12,56 +12,97 @@ * Delegates sorting to a strategy. */ class SortContext { - constructor(strategy) { - // TODO: Store strategy - // this.strategy = strategy; - } - - setStrategy(strategy) { - // TODO: Update strategy - } - - sort(array) { - // TODO: Delegate to strategy - // Return sorted copy, don't mutate original - throw new Error("Not implemented"); - } + constructor(strategy) { + this.strategy = strategy; + } + + setStrategy(strategy) { + this.strategy = strategy; + } + + sort(array) { + const arrayCopy = [...array]; + return this.strategy.sort(arrayCopy); + } } /** * Bubble Sort Strategy */ class BubbleSort { - sort(array) { - // TODO: Implement bubble sort - // Return new sorted array - - return ["NOT_IMPLEMENTED"]; // Broken: Replace with implementation - } + sort(array) { + for (let i = 0; i < array.length; i++) { + let completed = 0; + + for (let j = 0; j < array.length - 1; j++) { + if (array[j] > array[j + 1]) { + const temp = array[j + 1]; + array[j + 1] = array[j]; + array[j] = temp; + completed++; + } + } + + if (completed === 0) break; + } + + return array; + } } /** * Quick Sort Strategy */ class QuickSort { - sort(array) { - // TODO: Implement quick sort - // Return new sorted array + sort(array) { + if (array.length <= 1) return array; + + const pivot = array[0]; + const less = []; + const greater = []; - return []; // Broken: Replace with implementation - } + for (let i = 1; i < array.length; i++) { + const element = array[i]; + if (pivot > element) less.push(element) + else greater.push(element) + } + + return [...this.sort(less), pivot, ...this.sort(greater)] + } } /** * Merge Sort Strategy */ class MergeSort { - sort(array) { - // TODO: Implement merge sort - // Return new sorted array - - return []; // Broken: Replace with implementation - } + sort(array) { + if (array.length <= 1) return array; + + const mid = Math.floor(array.length / 2); + const left = array.slice(0, mid); + const right = array.slice(mid); + + const sortedLeft = this.sort(left); + const sortedRight = this.sort(right); + + return merge(sortedLeft, sortedRight); + + function merge(left, right) { + const result = []; + + while (left.length !== 0 && right.length !== 0) { + if (left[0] <= right[0]) { + result.push(left[0]); + left.splice(0, 1); + } else { + result.push(right[0]); + right.splice(0, 1); + } + } + + return result.concat(left, right); + } + } } // ============================================ @@ -74,71 +115,69 @@ class MergeSort { * Calculates prices using a strategy. */ class PricingContext { - constructor(strategy) { - // TODO: Store strategy - } - - setStrategy(strategy) { - // TODO: Update strategy - } - - calculateTotal(items) { - // TODO: Delegate to strategy - throw new Error("Not implemented"); - } + constructor(strategy) { + this.strategy = strategy; + } + + setStrategy(strategy) { + this.strategy = strategy; + } + + calculateTotal(items) { + return this.strategy.calculate(items); + } } /** * Regular Pricing (no discount) */ class RegularPricing { - calculate(items) { - // TODO: Sum all item prices - throw new Error("Not implemented"); - } + calculate(items) { + return items.reduce((a, item) => a + item.price, 0); + } } /** * Percentage Discount */ class PercentageDiscount { - constructor(percentage) { - // TODO: Store percentage (0-100) - // this.percentage = percentage; - } - - calculate(items) { - // TODO: Apply percentage discount - // total * (1 - percentage/100) - throw new Error("Not implemented"); - } + constructor(percentage) { + this.percentage = percentage; + } + + calculate(items) { + return items.reduce((a, item) => a + item.price, 0) * (1 - this.percentage / 100); + } } /** * Fixed Discount */ class FixedDiscount { - constructor(amount) { - // TODO: Store fixed discount amount - // this.amount = amount; - } - - calculate(items) { - // TODO: Subtract fixed amount from total - // Don't go below 0 - throw new Error("Not implemented"); - } + constructor(amount) { + this.amount = amount; + } + + calculate(items) { + const total = items.reduce((a, item) => a + item.price, 0); + return Math.max(total - this.amount, 0); + } } /** * Buy One Get One Free */ class BuyOneGetOneFree { - calculate(items) { - // TODO: Every second item is free - // Sort by price desc, charge only every other item - throw new Error("Not implemented"); - } + calculate(items) { + const newItems = [...items].sort((a, b) => b.price - a.price); + return newItems.reduce((acc, item, index) => { + if ((index + 1) % 2 !== 0) { + return acc + item.price + } + + return acc; + }, 0) + } } /** @@ -147,16 +186,18 @@ class BuyOneGetOneFree { * Different discount based on total. */ class TieredDiscount { - constructor(tiers) { - // TODO: Store tiers - // tiers = [{ threshold: 100, discount: 10 }, { threshold: 200, discount: 20 }] - // this.tiers = tiers; - } - - calculate(items) { - // TODO: Apply tier discount based on subtotal - throw new Error("Not implemented"); - } + constructor(tiers) { + this.tiers = tiers; + } + + calculate(items) { + const subtotal = items.reduce((acc, item) => acc + item.price, 0); + let tier = this.tiers.findLast(t => t.threshold <= subtotal); + + if (tier == null) return subtotal; + + return subtotal * (1 - tier.discount / 100) + } } // ============================================ @@ -167,18 +208,17 @@ class TieredDiscount { * Validation Context */ class ValidationContext { - constructor(strategy) { - // TODO: Store strategy - } - - setStrategy(strategy) { - // TODO: Update strategy - } - - validate(data) { - // TODO: Delegate to strategy - throw new Error("Not implemented"); - } + constructor(strategy) { + this.strategy = strategy; + } + + setStrategy(strategy) { + this.strategy = strategy; + } + + validate(data) { + return this.strategy.validate(data); + } } /** @@ -190,12 +230,27 @@ class ValidationContext { * - age: must be a number (any number is valid, no range check required) */ 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"); - } + validate(data) { + let valid = true; + let errors = []; + + if (!data.name) { + valid = false; + errors.push("Name is required") + } + + if (!data.email) { + valid = false; + errors.push("Email is required") + } + + if (!data.age) { + valid = false; + errors.push("Age is required") + } + + return {valid: valid, errors: errors} + } } /** @@ -205,11 +260,9 @@ class StrictValidation { * No validation rules - always passes. */ 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 - } + validate(data) { + return {valid: true, errors: []}; + } } // ============================================ @@ -222,43 +275,40 @@ class LenientValidation { * Register and retrieve strategies by name. */ class StrategyRegistry { - constructor() { - // TODO: Initialize registry map - // this.strategies = new Map(); - } - - register(name, strategy) { - // TODO: Store strategy by name - } - - get(name) { - // TODO: Return strategy by name - throw new Error("Not implemented"); - } - - has(name) { - // TODO: Check if strategy exists - throw new Error("Not implemented"); - } + constructor() { + this.strategies = new Map(); + } + + register(name, strategy) { + this.strategies.set(name, strategy); + } + + get(name) { + return this.strategies.get(name) || null; + } + + has(name) { + return this.strategies.has(name); + } } module.exports = { - // Sorting - SortContext, - BubbleSort, - QuickSort, - MergeSort, - // Pricing - PricingContext, - RegularPricing, - PercentageDiscount, - FixedDiscount, - BuyOneGetOneFree, - TieredDiscount, - // Validation - ValidationContext, - StrictValidation, - LenientValidation, - // Registry - StrategyRegistry, + // Sorting + SortContext, + BubbleSort, + QuickSort, + MergeSort, + // Pricing + PricingContext, + RegularPricing, + PercentageDiscount, + FixedDiscount, + BuyOneGetOneFree, + TieredDiscount, + // Validation + ValidationContext, + StrictValidation, + LenientValidation, + // Registry + StrategyRegistry, }; diff --git a/19-proxy-pattern/index.js b/19-proxy-pattern/index.js index 6d2bf2b..11dfc4a 100644 --- a/19-proxy-pattern/index.js +++ b/19-proxy-pattern/index.js @@ -10,31 +10,18 @@ * @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 - // - Otherwise, set the property - // - On 'get': return property value normally - - return new Proxy(target, { - set(obj, prop, value) { - // TODO: Implement set trap - // Check validators[prop](value) if validator exists - // Throw if validation fails - // Set property if passes - - // Broken: doesn't set at all (fails all tests) - return true; - }, - - get(obj, prop) { - // TODO: Implement get trap - // Broken: returns wrong value - return "NOT_IMPLEMENTED"; - }, - }); + return new Proxy(target, { + set(obj, prop, value) { + const validator = validators[prop]; + if (validator != null && !validator(value)) + throw new Error("Validation fails"); + obj[prop] = value; + }, + + get(obj, prop) { + return obj[prop]; + }, + }); } /** @@ -45,29 +32,31 @@ 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"); - }, - - set(obj, prop, value) { - // TODO: Log 'set' and set value - throw new Error("Not implemented"); - }, - - deleteProperty(obj, prop) { - // TODO: Log 'delete' and delete property - throw new Error("Not implemented"); - }, - - has(obj, prop) { - // TODO: Log 'has' and return result - throw new Error("Not implemented"); - }, - }); + return new Proxy(target, { + get(obj, prop) { + const value = obj[prop]; + logger('get', prop, value); + + return value; + }, + + set(obj, prop, value) { + logger('set', prop, value); + obj[prop] = value; + }, + + deleteProperty(obj, prop) { + logger('delete', prop, obj[prop]); + delete obj[prop]; + }, + + has(obj, prop) { + const value = Object.hasOwn(obj, prop); + logger('delete', prop, value); + + return value; + }, + }); } /** @@ -78,26 +67,25 @@ function createLoggingProxy(target, logger) { * @returns {Proxy} Proxy that caches method results */ function createCachingProxy(target, methodNames) { - // TODO: Implement caching proxy + const cache = new Map(); - // Create cache storage - // const cache = new Map(); + return new Proxy(target, { + get(obj, prop) { + if (!methodNames.includes(prop) || typeof obj[prop] !== 'function') return obj[prop]; - return new Proxy(target, { - get(obj, prop) { - // TODO: Implement get trap + return function (...args) { + const key = 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 value = obj[prop](...args); + cache.set(key, value); - throw new Error("Not implemented"); - }, - }); + return value; + } + }, + }); } /** @@ -110,31 +98,31 @@ function createCachingProxy(target, methodNames) { * @returns {Proxy} Proxy that enforces access control */ function createAccessProxy(target, permissions) { - // TODO: Implement access control proxy - - const { readable = [], writable = [] } = permissions; - - 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"; - }, - - set(obj, prop, value) { - // TODO: Check if prop is in writable - // Throw if not allowed - // Broken: doesn't actually set - return true; - }, - - deleteProperty(obj, prop) { - // TODO: Only allow if in writable - // Broken: doesn't delete - return true; - }, - }); + const {readable = [], writable = []} = permissions; + + return new Proxy(target, { + get(obj, prop) { + if (readable.includes(prop)) return obj[prop]; + + throw new Error(); + }, + + set(obj, prop, value) { + if (writable.includes(prop)) { + obj[prop] = value; + return true; + } + + throw new Error(); + }, + + deleteProperty(obj, prop) { + if (!writable.includes(prop)) return false; + + delete obj[prop]; + return true; + }, + }); } /** @@ -144,27 +132,29 @@ 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; - - return new Proxy( - {}, - { - get(obj, prop) { - // TODO: Load instance on first access - // if (!loaded) { instance = loader(); loaded = true; } - // return instance[prop] - throw new Error("Not implemented"); - }, - - set(obj, prop, value) { - // TODO: Load instance if needed, then set - throw new Error("Not implemented"); - }, - }, - ); + let instance = null; + let loaded = false; + + return new Proxy( + {}, + { + get(obj, prop) { + if (!loaded) { + instance = loader(); + loaded = true; + } + + return instance[prop]; + }, + + set(obj, prop, value) { + if (instance == null) + instance = loader(); + + instance[prop] = value; + }, + }, + ); } /** @@ -175,26 +165,25 @@ 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"); - }, - - deleteProperty(obj, prop) { - // TODO: Call onChange on delete - throw new Error("Not implemented"); - }, - }); + return new Proxy(target, { + set(obj, prop, value) { + onChange(prop, value, obj[prop]); + obj[prop] = value; + }, + + deleteProperty(obj, prop) { + const oldValue = obj[prop]; + onChange(prop, undefined, oldValue); + delete obj[prop]; + }, + }); } module.exports = { - createValidatingProxy, - createLoggingProxy, - createCachingProxy, - createAccessProxy, - createLazyProxy, - createObservableProxy, + createValidatingProxy, + createLoggingProxy, + createCachingProxy, + createAccessProxy, + createLazyProxy, + createObservableProxy, }; diff --git a/20-builder-pattern/index.js b/20-builder-pattern/index.js index 69a1e1a..5596157 100644 --- a/20-builder-pattern/index.js +++ b/20-builder-pattern/index.js @@ -8,86 +8,116 @@ * Builds SQL-like query strings. */ class QueryBuilder { - constructor() { - // TODO: Initialize state - // this.selectCols = []; - // this.fromTable = null; - // this.whereClauses = []; - // this.orderByClauses = []; - // this.limitCount = null; - } - - /** - * Select columns - * @param {...string} columns - Column names - * @returns {QueryBuilder} this - */ - select(...columns) { - // TODO: Store columns - throw new Error("Not implemented"); - } - - /** - * From table - * @param {string} table - Table name - * @returns {QueryBuilder} this - */ - from(table) { - // TODO: Store table name - throw new Error("Not implemented"); - } - - /** - * Add where clause - * @param {string} column - Column name - * @param {string} operator - Comparison operator - * @param {*} value - Value to compare - * @returns {QueryBuilder} this - */ - where(column, operator, value) { - // TODO: Store where clause - throw new Error("Not implemented"); - } - - /** - * Add order by clause - * @param {string} column - Column to order by - * @param {string} [direction='ASC'] - ASC or DESC - * @returns {QueryBuilder} this - */ - orderBy(column, direction = "ASC") { - // TODO: Store order by clause - throw new Error("Not implemented"); - } - - /** - * Set limit - * @param {number} count - Maximum rows - * @returns {QueryBuilder} this - */ - limit(count) { - // TODO: Store limit - throw new Error("Not implemented"); - } - - /** - * Build the query string - * @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"); - } - - /** - * Reset builder state - * @returns {QueryBuilder} this - */ - reset() { - // TODO: Reset all state - throw new Error("Not implemented"); - } + constructor() { + this.selectCols = []; + this.fromTable = null; + this.whereClauses = []; + this.orderByClauses = []; + this.limitCount = null; + } + + /** + * Select columns + * @param {...string} columns - Column names + * @returns {QueryBuilder} this + */ + select(...columns) { + this.selectCols.push(...columns) + return this; + } + + /** + * From table + * @param {string} table - Table name + * @returns {QueryBuilder} this + */ + from(table) { + this.fromTable = table; + return this; + } + + /** + * Add where clause + * @param {string} column - Column name + * @param {string} operator - Comparison operator + * @param {*} value - Value to compare + * @returns {QueryBuilder} this + */ + where(column, operator, value) { + this.whereClauses.push({column: column, operator: operator, value: value}); + return this; + } + + /** + * Add order by clause + * @param {string} column - Column to order by + * @param {string} [direction='ASC'] - ASC or DESC + * @returns {QueryBuilder} this + */ + orderBy(column, direction = "ASC") { + this.orderByClauses.push({column: column, direction: direction}) + return this; + } + + /** + * Set limit + * @param {number} count - Maximum rows + * @returns {QueryBuilder} this + */ + limit(count) { + this.limitCount = count; + return this; + } + + /** + * Build the query string + * @returns {string} SQL query string + */ + build() { + let query = "SELECT "; + + query += this.selectCols.length !== 0 ? this.selectCols.join(', ') : "* "; + + query += ` FROM ${this.fromTable}`; + + if (this.whereClauses.length !== 0) { + const wherePart = this.whereClauses + .map(w => `${w.column} ${w.operator} ${formatValue(w.value)}`) + .join(" AND "); + query += ` WHERE ${wherePart}`; + } + + if (this.orderByClauses.length !== 0) { + const orderPart = this.orderByClauses + .map(o => `${o.column} ${o.direction}`) + .join(", "); + query += ` ORDER BY ${orderPart}`; + } + + if (this.limitCount !== null) { + query += ` LIMIT ${this.limitCount}`; + } + + return query; + + function formatValue(val) { + return typeof val === 'string' ? `'${val}'` : val; + } + } + + /** + * Reset builder state + * @returns {QueryBuilder} this + */ + reset() { + this.selectCols.length = 0; + this.fromTable = null; + this.whereClauses.length = 0; + this.orderByClauses.length = 0; + this.limitCount = null; + + return this; + } } /** @@ -96,95 +126,119 @@ class QueryBuilder { * Builds HTML element strings. */ class HTMLBuilder { - constructor() { - // TODO: Initialize state - // this.tagName = 'div'; - // this.idAttr = null; - // this.classes = []; - // this.attributes = {}; - // this.innerContent = ''; - // this.children = []; - } - - /** - * Set tag name - * @param {string} name - HTML tag name - * @returns {HTMLBuilder} this - */ - tag(name) { - // TODO: Store tag name - throw new Error("Not implemented"); - } - - /** - * Set id attribute - * @param {string} id - Element ID - * @returns {HTMLBuilder} this - */ - id(id) { - // TODO: Store id - throw new Error("Not implemented"); - } - - /** - * Add classes - * @param {...string} classNames - Class names to add - * @returns {HTMLBuilder} this - */ - class(...classNames) { - // TODO: Store classes - throw new Error("Not implemented"); - } - - /** - * Add attribute - * @param {string} name - Attribute name - * @param {string} value - Attribute value - * @returns {HTMLBuilder} this - */ - attr(name, value) { - // TODO: Store attribute - throw new Error("Not implemented"); - } - - /** - * Set inner content - * @param {string} content - Text content - * @returns {HTMLBuilder} this - */ - content(content) { - // TODO: Store content - throw new Error("Not implemented"); - } - - /** - * Add child element - * @param {string} childHtml - Child HTML string - * @returns {HTMLBuilder} this - */ - child(childHtml) { - // TODO: Store child - throw new Error("Not implemented"); - } - - /** - * Build HTML string - * @returns {string} HTML element string - */ - build() { - // TODO: Build and return HTML string - // Format: content - throw new Error("Not implemented"); - } - - /** - * Reset builder state - * @returns {HTMLBuilder} this - */ - reset() { - // TODO: Reset all state - throw new Error("Not implemented"); - } + constructor() { + this.tagName = 'div'; + this.idAttr = null; + this.classes = []; + this.attributes = {}; + this.innerContent = ''; + this.children = []; + } + + /** + * Set tag name + * @param {string} name - HTML tag name + * @returns {HTMLBuilder} this + */ + tag(name) { + this.tagName = name; + return this; + } + + /** + * Set id attribute + * @param {string} id - Element ID + * @returns {HTMLBuilder} this + */ + id(id) { + this.idAttr = id; + return this; + } + + /** + * Add classes + * @param {...string} classNames - Class names to add + * @returns {HTMLBuilder} this + */ + class(...classNames) { + this.classes.push(...classNames) + return this; + } + + /** + * Add attribute + * @param {string} name - Attribute name + * @param {string} value - Attribute value + * @returns {HTMLBuilder} this + */ + attr(name, value) { + this.attributes[name] = value; + return this; + } + + /** + * Set inner content + * @param {string} content - Text content + * @returns {HTMLBuilder} this + */ + content(content) { + this.innerContent = content; + return this; + } + + /** + * Add child element + * @param {string} childHtml - Child HTML string + * @returns {HTMLBuilder} this + */ + child(childHtml) { + this.children.push(childHtml); + return this; + } + + /** + * Build HTML string + * @returns {string} HTML element string + */ + build() { + let html = `<${this.tagName}` + + if (this.idAttr != null) + html += ` id="${this.idAttr}"`; + + if (this.classes.length !== 0) + html += ` class="${this.classes.join(' ')}"`; + + if (Object.entries(this.attributes).length !== 0) { + const attr = Object.entries(this.attributes) + .map(([name, value]) => `${name}="${value}"`) + .join(' '); + html += ` ${attr}`; + } + + html += `>${this.innerContent}`; + if (this.children.length !== 0) { + const childrenHtml = this.children.join(""); + html += childrenHtml; + } + + return `${html}`; + } + + /** + * Reset builder state + * @returns {HTMLBuilder} this + */ + reset() { + this.tagName = 'div'; + this.idAttr = null; + this.classes = []; + this.attributes = {}; + this.innerContent = ''; + this.children = []; + + return this; + } } /** @@ -193,74 +247,74 @@ class HTMLBuilder { * Builds configuration objects. */ class ConfigBuilder { - constructor() { - // TODO: Initialize state - // this.config = { - // environment: 'development', - // database: null, - // features: [], - // logLevel: 'info' - // }; - } - - /** - * Set environment - * @param {string} env - Environment name - * @returns {ConfigBuilder} this - */ - setEnvironment(env) { - // TODO: Set environment - throw new Error("Not implemented"); - } - - /** - * Set database configuration - * @param {Object} dbConfig - Database config object - * @returns {ConfigBuilder} this - */ - setDatabase(dbConfig) { - // TODO: Set database config - throw new Error("Not implemented"); - } - - /** - * Enable a feature - * @param {string} feature - Feature name - * @returns {ConfigBuilder} this - */ - enableFeature(feature) { - // TODO: Add feature to list - throw new Error("Not implemented"); - } - - /** - * Disable a feature - * @param {string} feature - Feature name - * @returns {ConfigBuilder} this - */ - disableFeature(feature) { - // TODO: Remove feature from list - throw new Error("Not implemented"); - } - - /** - * Set log level - * @param {string} level - Log level - * @returns {ConfigBuilder} this - */ - setLogLevel(level) { - // TODO: Set log level - throw new Error("Not implemented"); - } - - /** - * Build configuration object - * @returns {Object} Configuration object - */ - build() { - // TODO: Return copy of config - throw new Error("Not implemented"); - } + constructor() { + this.config = { + environment: 'development', + database: null, + features: [], + logLevel: 'info' + }; + } + + /** + * Set environment + * @param {string} env - Environment name + * @returns {ConfigBuilder} this + */ + setEnvironment(env) { + this.config.environment = env; + return this; + } + + /** + * Set database configuration + * @param {Object} dbConfig - Database config object + * @returns {ConfigBuilder} this + */ + setDatabase(dbConfig) { + this.config.database = dbConfig; + return this; + } + + /** + * Enable a feature + * @param {string} feature - Feature name + * @returns {ConfigBuilder} this + */ + enableFeature(feature) { + this.config.features.push(feature); + return this; + } + + /** + * Disable a feature + * @param {string} feature - Feature name + * @returns {ConfigBuilder} this + */ + disableFeature(feature) { + const index = this.config.features.indexOf(feature); + if (index != null) + this.config.features.splice(index, 1); + return this; + } + + /** + * Set log level + * @param {string} level - Log level + * @returns {ConfigBuilder} this + */ + setLogLevel(level) { + this.config.logLevel = level; + return this; + } + + /** + * Build configuration object + * @returns {Object} Configuration object + */ + build() { + return {...this.config}; + } } /** @@ -269,70 +323,103 @@ class ConfigBuilder { * Builds HTTP request configurations. */ class RequestBuilder { - constructor(baseUrl = "") { - // TODO: Initialize state - } - - /** - * Set HTTP method - * @param {string} method - GET, POST, PUT, DELETE, etc. - * @returns {RequestBuilder} this - */ - method(method) { - throw new Error("Not implemented"); - } - - /** - * Set URL path - * @param {string} path - URL path - * @returns {RequestBuilder} this - */ - path(path) { - throw new Error("Not implemented"); - } - - /** - * Add query parameter - * @param {string} key - Parameter name - * @param {string} value - Parameter value - * @returns {RequestBuilder} this - */ - query(key, value) { - throw new Error("Not implemented"); - } - - /** - * Add header - * @param {string} key - Header name - * @param {string} value - Header value - * @returns {RequestBuilder} this - */ - header(key, value) { - throw new Error("Not implemented"); - } - - /** - * Set request body - * @param {*} body - Request body - * @returns {RequestBuilder} this - */ - body(body) { - throw new Error("Not implemented"); - } - - /** - * Build request configuration - * @returns {Object} Request config for fetch - */ - build() { - // TODO: Return fetch-compatible config - throw new Error("Not implemented"); - } + constructor(baseUrl = "") { + this.baseUrl = baseUrl; + this.queryParams = new Map(); + this.headerParams = new Map(); + this.pathStr = ""; + this.methodStr = "GET"; + this.bodyStr = null; + } + + /** + * Set HTTP method + * @param {string} method - GET, POST, PUT, DELETE, etc. + * @returns {RequestBuilder} this + */ + method(method) { + this.methodStr = method; + return this; + } + + /** + * Set URL path + * @param {string} path - URL path + * @returns {RequestBuilder} this + */ + path(path) { + this.pathStr = path; + return this; + } + + /** + * Add query parameter + * @param {string} key - Parameter name + * @param {string} value - Parameter value + * @returns {RequestBuilder} this + */ + query(key, value) { + this.queryParams.set(key, value); + return this; + } + + /** + * Add header + * @param {string} key - Header name + * @param {string} value - Header value + * @returns {RequestBuilder} this + */ + header(key, value) { + this.headerParams.set(key, value); + return this; + } + + /** + * Set request body + * @param {*} body - Request body + * @returns {RequestBuilder} this + */ + body(body) { + this.bodyStr = body; + return this; + } + + /** + * Build request configuration + * @returns {Object} Request config for fetch + */ + build() { + let url = this.baseUrl + this.pathStr; + + let query; + if (this.queryParams.size !== 0) { + const params = []; + + for (const [key, value] of this.queryParams) + params.push(`${key}=${value}`); + + query = params.join('&'); + } + + if (query) { + url += `?${query}`; + } + + const headers = []; + if (this.headerParams.size !== 0) { + for (const [key, value] of this.headerParams) { + headers[key] = value; + } + } + + return {url: url, method: this.methodStr, headers: headers, body: this.bodyStr} + } + } module.exports = { - QueryBuilder, - HTMLBuilder, - ConfigBuilder, - RequestBuilder, + QueryBuilder, + HTMLBuilder, + ConfigBuilder, + RequestBuilder, };