diff --git a/01-deep-clone/index.js b/01-deep-clone/index.js index e0281df..ec25b2c 100644 --- a/01-deep-clone/index.js +++ b/01-deep-clone/index.js @@ -13,29 +13,95 @@ function deepClone(value, visited = new WeakMap()) { // Step 1: Handle primitives (return as-is) // Primitives: null, undefined, number, string, boolean, symbol, bigint + switch(typeof value){ + case 'null': return null; + case 'boolean': + case 'number': + case 'bigint': + case 'string': + case 'symbol': + case 'function': + case 'undefined': + return value; + } // Step 2: Check for circular references using the visited WeakMap // If we've seen this object before, return the cached clone + if(visited.has(value)){ + return visited.get(value); + } // Step 3: Handle Date objects // Create a new Date with the same time value + if(value instanceof Date){ + const clone = new Date(value.getTime()); + visited.set(value, clone); + return clone; + } // Step 4: Handle RegExp objects // Create a new RegExp with the same source and flags + if(value instanceof RegExp){ + const clone = new RegExp(value.source, value.flags); + visited.set(value, clone); + return clone; + } // Step 5: Handle Map objects // Create a new Map and deep clone each key-value pair + if(value instanceof Map){ + const clone = new Map(); + visited.set(value, clone); + + for(let [key, v] of value.entries()){ + clone.set(deepClone(key, visited), deepClone(v, visited)); + } + + return clone; + } // Step 6: Handle Set objects // Create a new Set and deep clone each value + if(value instanceof Set){ + const clone = new Set(); + visited.set(value, clone); + + for(let v of value.values()){ + clone.add(deepClone(v, visited)); + } + + return clone; + } // Step 7: Handle Arrays // Create a new array and deep clone each element + if(Array.isArray(value)){ + const clone = []; + visited.set(value, clone); + + for(let i = 0; i{ + fn.apply(context, args); + }, delay); + }; // Step 3: Add a cancel() method to clear pending timeout + debounceFn.cancel = function(){ + clearTimeout(timeoutId); + timeoutId = null; + }; // Step 4: Return the debounced function - - // Return a placeholder that doesn't work - throw new Error("Not implemented"); + return debounceFn; } /** @@ -43,17 +55,41 @@ function throttle(fn, limit) { // - Whether we're currently in a throttle period // - The timeout ID for cleanup + let isThrottle = false; + let timeoutId; + let lastArgs; + let lastContext; + // 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 throttleFn = function(...args){ + lastContext = this; + lastArgs = args; + + if(!isThrottle){ + fn.apply(this, args); + + isThrottle = true; + + timeoutId = setTimeout(()=>{ + isThrottle = false; + }, limit); + } + }; // Step 3: Add a cancel() method to reset throttle state - // Step 4: Return the throttled function + throttleFn.cancel = function(){ + clearTimeout(timeoutId); + isThrottle = false; + lastArgs = null; + lastContext = null; + } - // Return a placeholder that doesn't work - throw new Error("Not implemented"); + // Step 4: Return the throttled function + return throttleFn; } module.exports = { debounce, throttle }; diff --git a/03-custom-bind/index.js b/03-custom-bind/index.js index 3e691f9..e1885c1 100644 --- a/03-custom-bind/index.js +++ b/03-custom-bind/index.js @@ -15,6 +15,9 @@ function customBind(fn, context, ...boundArgs) { // Step 1: Validate that fn is a function // Throw TypeError if not + if(typeof fn !== 'function'){ + throw new TypeError('first arg must be function'); + } // Step 2: Create the bound function // It should: @@ -26,14 +29,31 @@ function customBind(fn, context, ...boundArgs) { // When called as a constructor: // - `this` should be a new instance, not the bound context // - The prototype chain should be preserved + const bound = function(...args){ + const isCalledAsConstructor = this instanceof bound; + + const actualContext = isCalledAsConstructor ? this : context; + + const argsFn = [...boundArgs, ...args]; + + return fn.apply(actualContext, argsFn); + } // Step 4: Preserve the prototype for constructor usage // boundFunction.prototype = Object.create(fn.prototype) + if(fn.prototype){ + bound.prototype = Object.create(fn.prototype); - // Step 5: Return the bound function + Object.defineProperty(bound.prototype, 'constructor', { + value: bound, + enumerable: false, + writable: true, + configurable: true + }); + } - // Return placeholder that doesn't work - throw new Error("Not implemented"); + // Step 5: Return the bound function + return bound; } /** @@ -44,8 +64,32 @@ function customBind(fn, context, ...boundArgs) { */ // Uncomment and implement: -// Function.prototype.customBind = function(context, ...boundArgs) { -// // Your implementation -// }; +Function.prototype.customBind = function(context, ...boundArgs) { + const originalFn = this; + + if(typeof originalFn !== 'function'){ + throw new TypeError('Original function incompatible:'+ typeof originalFn); + } + + const bound = function(...args){ + const isConstructorCall = this instanceof bound; + + const actualContext = isConstructorCall ? this : context; + + const argsFn = boundArgs.concat(args); + + return originalFn.apply(actualContext, args); + }; + + if(originalFn.prototype){ + function Empty(){} + Empty.prototype = originalFn.prototype; + bound.prototype = new Empty(); + + bound.prototype.constructor = bound; + } + + return bound; +}; module.exports = { customBind }; diff --git a/04-memoization/index.js b/04-memoization/index.js index dc0f09b..ec8abaa 100644 --- a/04-memoization/index.js +++ b/04-memoization/index.js @@ -15,12 +15,16 @@ function memoize(fn, options = {}) { // Step 1: Extract options with defaults // const { maxSize, ttl, keyGenerator } = options; + const {maxSize, ttl, keyGenerator} = options; // Step 2: Create the cache (use Map for ordered keys) // const cache = new Map(); + const cache = new Map(); // Step 3: Create default key generator // Default: JSON.stringify(args) or args.join(',') + const defaultKeyGenerator = (...args)=>JSON.stringify(args); + const getKey = keyGenerator || defaultKeyGenerator; // Step 4: Create the memoized function // - Generate cache key from arguments @@ -28,6 +32,52 @@ function memoize(fn, options = {}) { // - If cached, return cached value // - If not cached, call fn and store result // - Handle maxSize eviction (remove oldest) + const isExpired = (entry)=>{ + if (!ttl || !entry.timestamp) return false; + return Date.now() - entry.timestamp > ttl; + }; + + const cleanExpired = () =>{ + if (!ttl) return; + for (const [key, entry] of cache.entries()) { + if (isExpired(entry)) { + cache.delete(key); + } + } + }; + + const memoFn = function(...args){ + const cacheKey = getKey(args); + + if(cache.has(cacheKey)){ + const entry = cache.get(cacheKey); + + if(!isExpired(entry)){ + return entry.value; + } + else{ + cache.delete(cacheKey); + } + } + + const result = fn.apply(this, args); + + cleanExpired(); + + if (maxSize && cache.size >= maxSize) { + const firstKey = cache.keys().next().value; + cache.delete(firstKey); + } + + const entry = { + value: result, + timestamp: ttl ? Date.now() : null + }; + + cache.set(cacheKey, entry); + + return result; + } // Step 5: Add cache control methods // memoized.cache = { @@ -36,22 +86,47 @@ function memoize(fn, options = {}) { // has: (key) => cache.has(key), // get size() { return cache.size; } // }; + memoFn.cache = { + clear: ()=>cache.clear(), + delete: (...args)=>{ + const key = getKey(args); + return cache.delete(key); + }, + has: (...args)=>{ + const key = getKey(args); + if (!cache.has(key)) return false; + + const entry = cache.get(key); + if (isExpired(entry)) { + cache.delete(key); + return false; + } + + return true; + }, + get size(){ + cleanExpired(); + return cache.size; + } + }; + // Step 6: Return memoized function + return memoFn; - // Return placeholder that doesn't work - const memoized = function () { - return undefined; - }; - memoized.cache = { - clear: () => {}, - delete: () => false, - has: () => false, - get size() { - return -1; - }, - }; - return memoized; + // // Return placeholder that doesn't work + // const memoized = function () { + // return undefined; + // }; + // memoized.cache = { + // clear: () => {}, + // delete: () => false, + // has: () => false, + // get size() { + // return -1; + // }, + // }; + // return memoized; } module.exports = { memoize }; diff --git a/05-promise-utilities/index.js b/05-promise-utilities/index.js index e59a8eb..1f09b85 100644 --- a/05-promise-utilities/index.js +++ b/05-promise-utilities/index.js @@ -12,12 +12,35 @@ function promiseAll(promises) { // Step 1: Convert iterable to array // const promiseArray = Array.from(promises); + const promiseArray = Array.from(promises); // Step 2: Handle empty array case // Return Promise.resolve([]) for empty input + if(promiseArray.length === 0){ + return Promise.resolve([]); + } // Step 3: Create a new Promise // return new Promise((resolve, reject) => { + return new Promise((resolve, reject)=>{ + const results = new Array(promiseArray.length); + let completed = 0; + + promiseArray.forEach((promise, index)=>{ + Promise.resolve(promise) + .then((val)=>{ + results[index] = val; + completed++; + + if(completed === promiseArray.length){ + resolve(results); + } + }) + .catch((err)=>{ + reject(err); + }); + }); + }) // Step 4: Track results and completion count // const results = new Array(promiseArray.length); @@ -30,8 +53,6 @@ function promiseAll(promises) { // - On reject: immediately reject the whole promise // }); - - return Promise.reject(new Error("Not implemented")); // Broken: Replace with your implementation } /** @@ -46,16 +67,26 @@ function promiseRace(promises) { // TODO: Implement promiseRace // Step 1: Convert iterable to array + const promiseArray = Array.from(promises); // Step 2: Handle empty array (return pending promise) // For empty array, return a promise that never settles + if(promiseArray.length === 0){ + return new Promise(()=>{}); + } // Step 3: Create a new Promise // The first promise to settle wins + return new Promise((resolve, reject)=>{ + promiseArray.forEach((promise)=>{ + Promise.resolve(promise) + .then(resolve) + .catch(reject); + }); + }); - // Step 4: For each promise, attach then/catch that resolves/rejects the race - return new Promise(() => {}); // Replace with your implementation + // Step 4: For each promise, attach then/catch that resolves/rejects the race } /** @@ -71,10 +102,36 @@ function promiseAllSettled(promises) { // TODO: Implement promiseAllSettled // Step 1: Convert iterable to array + const promiseArray = Array.from(promises); // Step 2: Handle empty array case + if (promiseArray.length === 0) { + return Promise.resolve([]); + } // Step 3: Create a new Promise + return new Promise((resolve)=>{ + const results = new Array(promiseArray.length); + + let settledCounter = 0; + + promiseArray.forEach((promise, index)=>{ + Promise.resolve(promise) + .then((val)=>{ + results[index] = {status: 'fulfilled', value: val}; + }) + .catch((err)=>{ + results[index] = {status: 'rejected', reason: err}; + }) + .finally(()=>{ + settledCounter++; + + if(settledCounter === promiseArray.length){ + resolve(results); + } + }); + }); + }); // Step 4: Track results and completion count // Each result is: { status: 'fulfilled', value } or { status: 'rejected', reason } @@ -84,8 +141,6 @@ function promiseAllSettled(promises) { // - 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 } /** @@ -101,10 +156,33 @@ function promiseAny(promises) { // TODO: Implement promiseAny // Step 1: Convert iterable to array + const promiseArray = Array.from(promises); // Step 2: Handle empty array (reject with AggregateError) + if (promiseArray.length === 0) { + return Promise.reject(new AggregateError([], 'promise array === 0')); + } // Step 3: Create a new Promise + return new Promise((resolve, reject)=>{ + const errors = new Array(promiseArray.length); + let rejectedCounter = 0; + + promiseArray.forEach((promise, index)=>{ + Promise.resolve(promise) + .then((val)=>{ + resolve(val); + }) + .catch((err)=>{ + errors[index] = err; + rejectedCounter++; + + if(rejectedCounter === promiseArray.length){ + reject(new AggregateError(errors, 'All promises were rejected')); + } + }); + }); + }); // Step 4: Track rejection count and errors // const errors = []; @@ -117,8 +195,6 @@ function promiseAny(promises) { // Note: AggregateError is created like: // new AggregateError(errorsArray, 'All promises were rejected') - - return Promise.reject(new AggregateError([], "No promises")); // Replace } module.exports = { promiseAll, promiseRace, promiseAllSettled, promiseAny }; diff --git a/06-async-queue/index.js b/06-async-queue/index.js index 2ab4d0a..2e99c6b 100644 --- a/06-async-queue/index.js +++ b/06-async-queue/index.js @@ -13,13 +13,13 @@ class AsyncQueue { constructor(options = {}) { // TODO: Initialize the queue // Step 1: Extract options with defaults - // this.concurrency = options.concurrency || 1; - // this.autoStart = options.autoStart !== false; + 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 + this.queue = []; // Pending tasks + this.running = 0; // Currently running count + this.paused = !this.autoStart; // Paused state + this.emptyCallbacks = []; // Callbacks for empty event } /** @@ -33,16 +33,31 @@ class AsyncQueue { // TODO: Implement add // Step 1: Create a new Promise and store its resolve/reject + let entryResolve, entryReject; + const promise = new Promise((res, rej)=>{ + entryResolve = res; + entryReject = rej; + }); // Step 2: Create task entry with: task, priority, resolve, reject + const taskEntry = { + task, + priority: options.priority ?? 0, + resolve: entryResolve, + reject: entryReject, + }; // Step 3: Add to queue (consider priority ordering) + this.queue.push(taskEntry); + this.queue.sort((a, b) => b.priority - a.priority); // Step 4: Try to process if autoStart and not paused + if(this.autoStart && !this.paused){ + this._process(); + } // Step 5: Return the promise - - return Promise.resolve(); // Replace with your implementation + return promise; } /** @@ -51,6 +66,8 @@ class AsyncQueue { start() { // TODO: Implement start // Set paused to false and trigger processing + this.paused = false; + this._process(); } /** @@ -59,6 +76,7 @@ class AsyncQueue { pause() { // TODO: Implement pause // Set paused to true + this.paused = true; } /** @@ -68,6 +86,7 @@ class AsyncQueue { // TODO: Implement clear // Empty the queue array // Optionally: reject pending promises with an error + this.queue.length = 0; } /** @@ -77,6 +96,7 @@ class AsyncQueue { onEmpty(callback) { // TODO: Implement onEmpty // Store callback to be called when size becomes 0 and nothing running + this.emptyCallbacks.push(callback); } /** @@ -85,7 +105,7 @@ class AsyncQueue { */ get size() { // TODO: Return queue length - throw new Error("Not implemented"); + return this.queue.length; } /** @@ -94,7 +114,7 @@ class AsyncQueue { */ get pending() { // TODO: Return running count - throw new Error("Not implemented"); + return this.running; } /** @@ -103,7 +123,7 @@ class AsyncQueue { */ get isPaused() { // TODO: Return paused state - throw new Error("Not implemented"); + return this.paused; } /** @@ -116,6 +136,21 @@ class AsyncQueue { // - Not paused // - Running count < concurrency // - Queue has items + while(this.running < this.concurrency && !this.paused && this.queue.length !== 0){ + const entry = this.queue.shift(); + this.running++; + + Promise.resolve(entry.task()) + .then( + value => entry.resolve(value), + error => entry.reject(error) + ) + .finally(()=>{ + this.running--; + this._checkEmpty(); + this._process(); + }); + } // Step 2: Take task from queue (respect priority) // Step 3: Increment running count // Step 4: Execute task and handle result @@ -130,6 +165,10 @@ class AsyncQueue { */ _checkEmpty() { // TODO: If queue is empty and nothing running, call empty callbacks + if(this.queue.length === 0 && this.running === 0){ + this.emptyCallbacks.forEach(callback => callback()); + this.emptyCallbacks.length = 0; + } } } diff --git a/06-async-queue/index.test.js b/06-async-queue/index.test.js index 04f3dc9..b4029c7 100644 --- a/06-async-queue/index.test.js +++ b/06-async-queue/index.test.js @@ -251,6 +251,8 @@ describe("AsyncQueue", () => { await queue.add(async () => "task"); + await delay(10); + expect(emptyCalled).toBe(true); }); @@ -263,6 +265,8 @@ describe("AsyncQueue", () => { await queue.add(async () => {}); + await delay(10); + expect(calls).toEqual(["callback1", "callback2"]); }); diff --git a/07-retry-with-backoff/index.js b/07-retry-with-backoff/index.js index ed7fd75..cf8a7fb 100644 --- a/07-retry-with-backoff/index.js +++ b/07-retry-with-backoff/index.js @@ -18,19 +18,46 @@ async function retry(fn, options = {}) { // TODO: Implement retry with backoff // Step 1: Extract options with defaults - // const { - // maxRetries = 3, - // initialDelay = 1000, - // maxDelay = 30000, - // backoff = 'exponential', - // jitter = false, - // retryIf = () => true, - // onRetry = () => {} - // } = options; + const { + maxRetries = 3, + initialDelay = 1000, + maxDelay = 30000, + backoff = 'exponential', + jitter = false, + retryIf = () => true, + onRetry = () => {} + } = options; // Step 2: Initialize attempt counter and last error + let lastError; // Step 3: Loop up to maxRetries + 1 (initial attempt + retries) + for(let attempt = 1; attempt <= maxRetries + 1; attempt++){ + try{ + const result = await fn(); + return result; + } + catch (error){ + lastError = error; + + const shouldRetry = attempt <= maxRetries && retryIf(error); + + if(!shouldRetry){ + throw error; + } + + onRetry(error, attempt); + + let delay = calculateDelay(backoff, attempt, initialDelay); + + delay = Math.min(delay, maxDelay); + + if(jitter){ + delay = applyJitter(delay); + } + await sleep(delay); + } + } // Step 4: Try to execute fn // - On success: return result @@ -46,7 +73,7 @@ async function retry(fn, options = {}) { // Step 6: If all retries exhausted, throw last error - throw new Error("Not implemented"); // Replace with your implementation + throw lastError; // Replace with your implementation } /** @@ -64,7 +91,15 @@ function calculateDelay(strategy, attempt, 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': + default: + return initialDelay * Math.pow(2, attempt - 1); + } } /** @@ -77,7 +112,7 @@ function applyJitter(delay) { // TODO: Add 0-25% random jitter // return delay * (1 + Math.random() * 0.25); - throw new Error("Not implemented"); + return delay * (1 + Math.random() * 0.25); } /** diff --git a/07-retry-with-backoff/index.test.js b/07-retry-with-backoff/index.test.js index a4ca6c8..1a76db9 100644 --- a/07-retry-with-backoff/index.test.js +++ b/07-retry-with-backoff/index.test.js @@ -1,5 +1,10 @@ const { retry, calculateDelay, applyJitter } = require("./index"); +function preventUnhandledRejection(promise) { + promise.catch(() => {}); + return promise; +} + describe("retry", () => { beforeEach(() => { jest.useFakeTimers(); @@ -44,7 +49,7 @@ describe("retry", () => { const error = new Error("persistent failure"); const fn = jest.fn().mockRejectedValue(error); - const promise = retry(fn, { maxRetries: 2, initialDelay: 100 }); + const promise = preventUnhandledRejection(retry(fn, { maxRetries: 2, initialDelay: 100 })); await jest.advanceTimersByTimeAsync(0); // attempt 1 await jest.advanceTimersByTimeAsync(100); // attempt 2 @@ -59,7 +64,7 @@ describe("retry", () => { test("should respect maxRetries", async () => { const fn = jest.fn().mockRejectedValue(new Error("fail")); - const promise = retry(fn, { maxRetries: 5, initialDelay: 10 }); + const promise = preventUnhandledRejection(retry(fn, { maxRetries: 5, initialDelay: 10 })); for (let i = 0; i < 10; i++) { await jest.advanceTimersByTimeAsync(100); @@ -72,7 +77,7 @@ describe("retry", () => { test("should handle maxRetries of 0", async () => { const fn = jest.fn().mockRejectedValue(new Error("fail")); - const promise = retry(fn, { maxRetries: 0 }); + const promise = preventUnhandledRejection(retry(fn, { maxRetries: 0 })); await jest.advanceTimersByTimeAsync(0); await expect(promise).rejects.toThrow(); @@ -102,9 +107,9 @@ describe("retry", () => { test("should not retry when retryIf returns false", async () => { const fn = jest.fn().mockRejectedValue({ status: 400 }); - const promise = retry(fn, { + const promise = preventUnhandledRejection(retry(fn, { retryIf: (error) => error.status >= 500, - }); + })); await jest.advanceTimersByTimeAsync(0); await expect(promise).rejects.toEqual({ status: 400 }); @@ -192,12 +197,12 @@ describe("retry with backoff strategies", () => { const delays = []; const fn = jest.fn().mockRejectedValue(new Error("fail")); - const promise = retry(fn, { + const promise = preventUnhandledRejection(retry(fn, { maxRetries: 5, initialDelay: 1000, maxDelay: 5000, backoff: "exponential", - }); + })); // Track delays by advancing time // exponential would be: 1000, 2000, 4000, 8000, 16000 diff --git a/08-event-emitter/index.js b/08-event-emitter/index.js index b7c9a7c..a4f6543 100644 --- a/08-event-emitter/index.js +++ b/08-event-emitter/index.js @@ -6,7 +6,7 @@ class EventEmitter { constructor() { // TODO: Initialize event storage - // this.events = new Map(); // or {} + this.events = new Map(); } /** @@ -19,12 +19,16 @@ class EventEmitter { // TODO: Implement on // Step 1: Get or create the listeners array for this event + if(!this.events.has(event)){ + this.events.set(event, []); + } // Step 2: Add the listener to the array + const listeners = this.events.get(event); + listeners.push(listener); // Step 3: Return this for chaining - - return null; // Broken: should return this + return this; } /** @@ -37,13 +41,35 @@ class EventEmitter { // TODO: Implement off // Step 1: Get the listeners array for this event + const listeners = this.events.get(event); + + if(!listeners){ + return this; + } // Step 2: Find and remove the listener // Note: Handle wrapped 'once' listeners + let indexToRemove = -1; - // Step 3: Return this for chaining + for(let i = listeners.length - 1; i >= 0; i--){ + const currentListener = listeners[i]; + + if(currentListener === listener || currentListener.listener === listener){ + indexToRemove = i; + break; + } + } - return null; // Broken: should return this + if(indexToRemove !== -1){ + listeners.splice(indexToRemove, 1); + } + + if(listeners.length === 0){ + this.events.delete(event); + } + + // Step 3: Return this for chaining + return this; } /** @@ -56,15 +82,32 @@ class EventEmitter { // TODO: Implement emit // Step 1: Get the listeners array for this event + const listeners = this.events.get(event); // Step 2: If no listeners, return false + if(!listeners || listeners.length === 0){ + return false; + } // Step 3: Call each listener with the arguments // Make a copy of the array to handle removals during emit + const listenersCopy = [...listeners]; + + for (const listener of listenersCopy) { + try{ + if(typeof listener === 'function'){ + listener.apply(this, args); + } + } + catch(error){ + if(event !== 'error' && this.events.has('error')){ + this.emit('error', error); + } + } + } // Step 4: Return true - - throw new Error("Not implemented"); + return true; } /** @@ -79,14 +122,19 @@ class EventEmitter { // Step 1: Create a wrapper function that: // - Removes itself after being called // - Calls the original listener with arguments + const wrapper = (...args) => { + this.off(event, wrapper); + listener.apply(this, args); + }; // Step 2: Store reference to original listener for 'off' to work + wrapper.listener = listener; // Step 3: Register the wrapper with 'on' + this.on(event, wrapper); // Step 4: Return this for chaining - - return null; // Broken: should return this + return this; } /** @@ -96,11 +144,14 @@ class EventEmitter { */ 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 + if(event !== undefined){ + this.events.delete(event); + } + else{ + this.events.clear(); + } + + return this; } /** @@ -110,10 +161,10 @@ class EventEmitter { */ listeners(event) { // TODO: Implement listeners + const listeners = this.events.get(event); // Return copy of listeners array, or empty array if none - - throw new Error("Not implemented"); + return listeners ? [...listeners] : []; } /** @@ -123,8 +174,8 @@ class EventEmitter { */ listenerCount(event) { // TODO: Implement listenerCount - - throw new Error("Not implemented"); + const listeners = this.events.get(event); + return listeners ? listeners.length : 0; } } diff --git a/09-observable/index.js b/09-observable/index.js index a7e8f15..65069d3 100644 --- a/09-observable/index.js +++ b/09-observable/index.js @@ -11,6 +11,7 @@ class Observable { constructor(subscribeFn) { // TODO: Store the subscribe function // this._subscribeFn = subscribeFn; + this._subscribeFn = subscribeFn; } /** @@ -23,19 +24,98 @@ class Observable { // Step 1: Normalize observer (handle function shorthand) // If observer is a function, wrap it: { next: observer } + let normalizedObserver; + if(typeof observer === 'function'){ + normalizedObserver = {next: observer}; + } + else{ + normalizedObserver = 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 + let isStopped = false; + let unsubscribeFn = null; + + const subscriber = { + next: (value)=>{ + if(!isStopped && normalizedObserver.next){ + try{ + normalizedObserver.next(value); + } + catch(error){ + this.error(error); + } + } + }, + error: (error)=>{ + if(!isStopped){ + isStopped = true; + if (normalizedObserver.error) { + try{ + normalizedObserver.error(error); + } + catch (e){ + } + } + if(unsubscribeFn){ + unsubscribeFn(); + unsubscribeFn = null; + } + } + }, + complete: ()=>{ + if(!isStopped){ + isStopped = true; + if(normalizedObserver.complete){ + try{ + normalizedObserver.complete(); + } + catch(error){ + } + } + + if(unsubscribeFn){ + unsubscribeFn(); + unsubscribeFn = null; + } + } + } + }; // Step 3: Call the subscribe function with the subscriber + try{ + const cleanup = this._subscribeFn(subscriber); + + if(typeof cleanup === 'function'){ + unsubscribeFn = cleanup; + } + else if(cleanup && typeof cleanup.unsubscribe === 'function'){ + unsubscribeFn = () => cleanup.unsubscribe(); + } + } + catch(error){ + subscriber.error(error); + } // Step 4: Handle cleanup function returned by subscribeFn // Step 5: Return subscription object with unsubscribe method - throw new Error("Not implemented"); + return{ + unsubscribe: ()=>{ + if(!isStopped){ + isStopped = true; + + if(unsubscribeFn){ + unsubscribeFn(); + unsubscribeFn = null; + } + } + } + }; } /** @@ -50,8 +130,23 @@ class Observable { // - Subscribes to source (this) // - Calls fn on each value // - Emits transformed value + return new Observable((subscriber)=>{ + const subscription = this.subscribe({ + next: (value)=>{ + try{ + const transformedValue = fn(value); + subscriber.next(transformedValue); + } + catch(error){ + subscriber.error(error); + } + }, + error: (error)=>subscriber.error(error), + complete: ()=>subscriber.complete() + }); - return new Observable(() => {}); // Broken: Replace with implementation + return ()=>subscription.unsubscribe(); + }); } /** @@ -66,7 +161,25 @@ class Observable { // - Subscribes to source (this) // - Only emits values where predicate returns true - return new Observable(() => {}); // Broken: Replace with implementation + return new Observable((subscriber)=>{ + const subscription = this.subscribe({ + next: (value)=>{ + try{ + if(predicate(value)){ + subscriber.next(value); + } + } + catch (error){ + subscriber.error(error); + } + }, + error: (error)=>subscriber.error(error), + complete: ()=>subscriber.complete() + }); + + return ()=>subscription.unsubscribe(); + }); + } /** @@ -82,7 +195,31 @@ class Observable { // - Emits first `count` values // - Completes after `count` values - return new Observable(() => {}); // Broken: Replace with implementation + return new Observable((subscriber)=>{ + if(count <= 0){ + subscriber.complete(); + return; + } + + let taken = 0; + const subscription = this.subscribe({ + next: (value)=>{ + if(taken < count){ + subscriber.next(value); + taken++; + + if(taken >= count){ + subscriber.complete(); + subscription.unsubscribe(); + } + } + }, + error: (error)=>subscriber.error(error), + complete: ()=>subscriber.complete() + }); + + return ()=>subscription.unsubscribe(); + }); } /** @@ -98,7 +235,23 @@ class Observable { // - Ignores first `count` values // - Emits remaining values - return new Observable(() => {}); // Broken: Replace with implementation + return new Observable((subscriber)=>{ + let skipped = 0; + const subscription = this.subscribe({ + next: (value)=>{ + if(skipped >= count){ + subscriber.next(value); + } + else{ + skipped++; + } + }, + error: (error)=>subscriber.error(error), + complete: ()=>subscriber.complete() + }); + + return ()=>subscription.unsubscribe(); + }); } /** @@ -114,8 +267,13 @@ class Observable { // - Completes after last element return new Observable((subscriber) => { - // subscriber.next(...) for each - // subscriber.complete() + for(const value of array){ + if (subscriber.isStopped) break; + subscriber.next(value); + } + subscriber.complete(); + + return () => {}; }); } diff --git a/10-lru-cache/index.js b/10-lru-cache/index.js index f087769..aba7edb 100644 --- a/10-lru-cache/index.js +++ b/10-lru-cache/index.js @@ -12,8 +12,10 @@ class LRUCache { // TODO: Initialize the cache // Step 1: Store capacity // this.capacity = capacity; + this.capacity = capacity; // Step 2: Create storage (Map recommended) // this.cache = new Map(); + this.cache = new Map(); } /** @@ -25,15 +27,21 @@ class LRUCache { // TODO: Implement get // Step 1: Check if key exists + if(!this.cache.has(key)){ + return undefined; + } // Step 2: If exists: // - Get the value // - Move to end (most recent) // - Return value + const value = this.cache.get(key); - // Step 3: If not exists, return undefined + this.cache.delete(key); + this.cache.set(key, value); - throw new Error("Not implemented"); + // Step 3: If not exists, return undefined + return value; } /** @@ -44,8 +52,16 @@ class LRUCache { put(key, value) { // TODO: Implement put // Step 1: If key already exists, delete it first (to update position) + if(this.cache.has(key)){ + this.cache.delete(key); + } // Step 2: If at capacity, evict least recently used (first item) + if(this.cache.size >= this.capacity){ + const firstKey = this.cache.keys().next().value; + this.cache.delete(firstKey); + } // Step 3: Add the new key-value pair (goes to end = most recent) + this.cache.set(key, value); } /** @@ -55,8 +71,7 @@ class LRUCache { */ has(key) { // TODO: Implement has - - throw new Error("Not implemented"); + return this.cache.has(key); } /** @@ -66,8 +81,7 @@ class LRUCache { */ delete(key) { // TODO: Implement delete - - throw new Error("Not implemented"); + return this.cache.delete(key); } /** @@ -75,7 +89,7 @@ class LRUCache { */ clear() { // TODO: Implement clear - throw new Error("Not implemented"); + this.cache.clear(); } /** @@ -84,8 +98,7 @@ class LRUCache { */ get size() { // TODO: Return current size - - throw new Error("Not implemented"); + return this.cache.size; } /** @@ -94,8 +107,7 @@ class LRUCache { */ keys() { // TODO: Return array of keys - - throw new Error("Not implemented"); + return Array.from(this.cache.keys()); } /** @@ -104,8 +116,7 @@ class LRUCache { */ values() { // TODO: Return array of values - - throw new Error("Not implemented"); + return Array.from(this.cache.values()); } } diff --git a/11-singleton/index.js b/11-singleton/index.js index 3c98305..3813b61 100644 --- a/11-singleton/index.js +++ b/11-singleton/index.js @@ -12,6 +12,7 @@ class Singleton { // Step 1: Create a static property to hold the instance // static instance = null; + static instance = null; // Step 2: Create a getInstance static method // - Check if instance exists @@ -20,19 +21,24 @@ class Singleton { static getInstance() { // TODO: Implement getInstance - throw new Error("Not implemented"); + if(!Singleton.instance){ + Singleton.instance = new Singleton(); + } + return Singleton.instance; } // Step 3: Optionally prevent direct instantiation - // constructor() { - // if (Singleton.instance) { - // throw new Error('Use Singleton.getInstance()'); - // } - // } + constructor() { + if (Singleton.instance) { + throw new Error('Use Singleton.getInstance()'); + } + this.createdAt = new Date(); + } // Step 4: Add a reset method for testing static resetInstance() { // TODO: Reset the instance to null + Singleton.instance = null; } } @@ -48,23 +54,28 @@ function createSingleton(Class) { // TODO: Implement createSingleton // Step 1: Create a closure variable to hold the instance - // let instance = null; + let instance = null; // Step 2: Return an object with getInstance method // getInstance should: // - 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"); + if(!instance){ + instance = new Class(...args); + } + return instance; }, - resetInstance: () => { + resetInstance: ()=>{ // TODO: Implement + instance = null; }, }; } diff --git a/12-factory-pattern/index.js b/12-factory-pattern/index.js index 3e0bd14..421d30a 100644 --- a/12-factory-pattern/index.js +++ b/12-factory-pattern/index.js @@ -71,11 +71,31 @@ const ShapeFactory = { */ create(type, options) { // TODO: Implement factory logic + switch(type.toLowerCase()){ + case 'circle': + if(!options.radius){ + throw new Error('radius required'); + } + return new Circle(options); + + case 'rectangle': + if(!options.width || !options.height){ + throw new Error('width and height required'); + } + return new Rectangle(options); + + case 'triangle': + if(!options.base || !options.height){ + throw new Error('base and height required'); + } + return new Triangle(options); + + default: + throw new Error('unknown type'); + } // Use switch or object lookup to create the right shape // Throw error for unknown types - - return null; // Replace with implementation }, }; @@ -88,6 +108,7 @@ class Factory { constructor() { // TODO: Initialize registry // this.registry = new Map(); + this.registry = new Map(); } /** @@ -101,6 +122,12 @@ class Factory { register(type, Class, options = {}) { // TODO: Implement register // Store the class and options in the registry + if(typeof Class !== 'function'){ + throw new Error('must be a constructor function'); + } + + this.registry.set(type, { Class, options }); + return this; } /** @@ -110,8 +137,7 @@ class Factory { */ unregister(type) { // TODO: Implement unregister - - throw new Error("Not implemented"); + return this.registry.delete(type); } /** @@ -124,16 +150,37 @@ class Factory { // TODO: Implement create // Step 1: Check if type is registered + if(!this.registry.has(type)){ + throw new Error('not registered'); + } // Step 2: Get the class and options + const {Class, options} = this.registry.get(type); // Step 3: Validate required fields (if specified) + if(options.required){ + for(const field of options.required){ + if(!(field in args)){ + throw new Error('Missing field'); + } + } + } // Step 4: Run custom validation (if specified) + if(options.validate && typeof options.validate === 'function'){ + const validationResult = options.validate(args); + if(validationResult !== true){ + throw new Error('Validation failed'); + } + } // Step 5: Create and return instance - - return null; // Replace with implementation + try{ + return new Class(args); + } + catch(error){ + throw new Error('failed to create instance'); + } } /** @@ -143,8 +190,7 @@ class Factory { */ has(type) { // TODO: Implement has - - throw new Error("Not implemented"); + return this.registry.has(type); } /** @@ -153,8 +199,7 @@ class Factory { */ getTypes() { // TODO: Implement getTypes - - throw new Error("Not implemented"); + return Array.from(this.registry.keys()); } /** @@ -162,7 +207,8 @@ class Factory { */ clear() { // TODO: Implement clear - throw new Error("Not implemented"); + this.registry.clear(); + return this; } } diff --git a/13-decorator-pattern/index.js b/13-decorator-pattern/index.js index 6a3646f..a39204a 100644 --- a/13-decorator-pattern/index.js +++ b/13-decorator-pattern/index.js @@ -14,6 +14,16 @@ function withLogging(fn) { // TODO: Implement withLogging // Step 1: Return a new function that wraps fn + return function(...args){ + const funcName = fn.name || 'anonymous'; + log(`calling ${funcName} with arguments: ${JSON.stringify(args)}`); + + const result = fn.apply(this, args); + + log(`${funcName} returned: ${JSON.stringify(result)}`); + + return result; + }; // Step 2: Log the function name and arguments @@ -24,9 +34,6 @@ function withLogging(fn) { // Step 5: Return the result // Note: Preserve 'this' context using apply/call - - // Broken: throws error - throw new Error("Not implemented"); } /** @@ -41,6 +48,18 @@ function withTiming(fn) { // TODO: Implement withTiming // Step 1: Return a new function + return function(...args){ + const start = performance.now ? performance.now() : Date.now(); + + const result = fn.apply(this, args); + + const end = performance.now ? performance.now() : Date.now(); + const duration = end - start; + const funcName = fn.name || 'anonymous'; + log(`${funcName} executed in ${duration.toFixed(2)}ms`); + + return result; + }; // Step 2: Record start time (performance.now() or Date.now()) @@ -49,8 +68,6 @@ function withTiming(fn) { // Step 4: Calculate and log duration // Step 5: Return result - - return () => undefined; // Broken placeholder } /** @@ -66,6 +83,23 @@ function withRetry(fn, maxRetries = 3) { // TODO: Implement withRetry // Step 1: Return a new function + return function(...args){ + let lastError; + + for(let attempt = 0; attempt <= maxRetries; attempt++){ + try { + const result = fn.apply(this, args); + return result; + } + catch (error){ + lastError = error; + if(attempt === maxRetries){ + throw lastError; + } + } + } + throw lastError; + }; // Step 2: Track attempt count @@ -75,8 +109,6 @@ function withRetry(fn, maxRetries = 3) { // - On failure, increment attempts and continue // Step 4: If all retries fail, throw the last error - - return () => undefined; // Broken placeholder } /** @@ -91,8 +123,20 @@ function withMemoize(fn) { // TODO: Implement withMemoize // Similar to memoization assignment but as a decorator - - return () => undefined; // Broken placeholder + const cache = new Map(); + + return function(...args) { + const key = JSON.stringify(args); + + if (cache.has(key)) { + return cache.get(key); + } + + const result = fn.apply(this, args); + cache.set(key, result); + + return result; + }; } /** @@ -108,14 +152,21 @@ function withValidation(fn, validator) { // TODO: Implement withValidation // Step 1: Return a new function + return function(...args){ + const isValid = validator.apply(this, args); + + if(!isValid){ + throw new Error('Validation failed'); + } + + return fn.apply(this, args); + }; // Step 2: Call validator with arguments // Step 3: If validation fails, throw error // Step 4: If passes, call original function - - return () => undefined; // Broken placeholder } /** @@ -131,15 +182,35 @@ function withCache(obj, methodName) { // TODO: Implement withCache // Step 1: Get the original method + const originalMethod = obj[methodName]; + + if(typeof originalMethod !== 'function'){ + throw new Error(`no obj`); + } // Step 2: Create a cache (Map) + const cache = new Map(); // Step 3: Replace the method with a caching wrapper + obj[methodName] = function(...args){ + const key = `${methodName}:${JSON.stringify(args)}`; + + if (cache.has(key)) { + return cache.get(key); + } + + const result = originalMethod.apply(this, args); + + cache.set(key, result); + + return result; + }; - // Step 4: Return the object + obj[methodName].clearCache = function(){ + cache.clear(); + }; - // Broken: deletes the method instead of caching it - delete obj[methodName]; + // Step 4: Return the object return obj; } @@ -158,9 +229,10 @@ function compose(...decorators) { // Return a function that takes fn and applies all decorators // Example: compose(a, b, c)(fn) = a(b(c(fn))) - - return (fn) => { - throw new Error("Not implemented"); + return function(fn){ + return decorators.reduceRight((wrappedFn, decorator)=>{ + return decorator(wrappedFn); + },fn); }; } @@ -176,9 +248,10 @@ function pipe(...decorators) { // TODO: Implement pipe // Same as compose but left-to-right - - return (fn) => { - throw new Error("Not implemented"); + return function(fn){ + return decorators.reduce((wrappedFn, decorator)=>{ + return decorator(wrappedFn); + },fn); }; } diff --git a/14-middleware-pipeline/index.js b/14-middleware-pipeline/index.js index 8db7620..3568aa7 100644 --- a/14-middleware-pipeline/index.js +++ b/14-middleware-pipeline/index.js @@ -6,7 +6,7 @@ class Pipeline { constructor() { // TODO: Initialize middleware array - // this.middleware = []; + this.middleware = []; } /** @@ -18,12 +18,15 @@ class Pipeline { // TODO: Implement use // Step 1: Validate fn is a function + if(typeof fn !== 'function'){ + throw new Error('must be a function'); + } // Step 2: Add to middleware array + this.middleware.push(fn); // Step 3: Return this for chaining - - return null; // Broken: should return this + return this; } /** @@ -40,13 +43,30 @@ class Pipeline { // - If no middleware, resolve // - Otherwise, call middleware with context and next function // - next = () => dispatch(index + 1) + const dispatch = (index)=>{ + if(index >= this.middleware.length){ + return Promise.resolve(); + } + + const middleware = this.middleware[index]; + + try{ + const next = ()=>dispatch(index + 1); + + const result = middleware(context, next); + + return Promise.resolve(result); + } + catch (error){ + return Promise.reject(error); + } + }; + // 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")); + return dispatch(0); } /** @@ -74,6 +94,15 @@ function compose(middleware) { // TODO: Implement compose // Validate all items are functions + if(!Array.isArray(middleware)){ + throw new TypeError('must be an array'); + } + + for(const fn of middleware){ + if(typeof fn !== 'function'){ + throw new TypeError('must be composed of functions'); + } + } // Return a function that: // - Takes context @@ -81,17 +110,26 @@ function compose(middleware) { // - Returns dispatch(0) return function (context) { - function dispatch(index) { - // TODO: Implement dispatch - - // 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 - - // Broken: rejects instead of resolving - return Promise.reject(new Error("Not implemented")); + let currentIndex = -1; + + function dispatch(index){ + if(index <= currentIndex){ + return Promise.reject(new Error('stackoverflow')); + } + currentIndex = index; + + const fn = middleware[index]; + + if(!fn){ + return Promise.resolve(); + } + + try{ + return Promise.resolve(fn(context, ()=>dispatch(index + 1))); + } + catch (error){ + return Promise.reject(error); + } } return dispatch(0); @@ -113,8 +151,15 @@ function when(condition, middleware) { // - If true, runs middleware // - If false, just calls next() - return (ctx, next) => { - throw new Error("Not implemented"); + return (ctx, next)=>{ + if(condition(ctx)){ + const result = middleware(ctx, next); + return Promise.resolve(result); + } + else{ + const result = next(); + return Promise.resolve(result); + } }; } @@ -132,7 +177,16 @@ function errorMiddleware(errorHandler) { // - Calls errorHandler if error thrown return async (ctx, next) => { - throw new Error("Not implemented"); + try{ + const result = next(); + return Promise.resolve(result).catch(error=>{ + errorHandler(error, ctx); + }); + } + catch(error){ + errorHandler(error, ctx); + return Promise.resolve(); + } }; } diff --git a/15-dependency-injection/index.js b/15-dependency-injection/index.js index e4995b5..5f7ac39 100644 --- a/15-dependency-injection/index.js +++ b/15-dependency-injection/index.js @@ -4,7 +4,7 @@ class Container { constructor() { // TODO: Initialize registry - // this.registry = new Map(); + this.registry = new Map(); } /** @@ -19,6 +19,19 @@ class Container { // TODO: Implement register // Store in registry: // { type: 'class', Class, dependencies, singleton, instance: null } + if(typeof Class !== 'function'){ + throw new Error('must be function'); + } + + this.registry.set(name,{ + type: 'class', + Class, + dependencies, + singleton: !!options.singleton, + instance: null + }); + + return this; } /** @@ -30,6 +43,16 @@ class Container { // TODO: Implement registerInstance // Store in registry: // { type: 'instance', instance } + if(instance === undefined || instance === null){ + throw new Error('cannot be null or undefined'); + } + + this.registry.set(name,{ + type: 'instance', + instance + }); + + return this; } /** @@ -43,6 +66,19 @@ class Container { // TODO: Implement registerFactory // Store in registry: // { type: 'factory', factory, dependencies, singleton, instance: null } + if(typeof factory !== 'function'){ + throw new Error('must be a function'); + } + + this.registry.set(name,{ + type: 'factory', + factory, + dependencies, + singleton: !!options.singleton, + instance: null + }); + + return this; } /** @@ -56,13 +92,52 @@ class Container { // Step 1: Check if service is registered // Throw error if not found + if(!this.registry.has(name)){ + throw new Error('not registered'); + } // Step 2: Check for circular dependencies // If name is already in resolutionStack, throw error + if(resolutionStack.has(name)){ + throw new Error('Circular dependency detected'); + } // Step 3: Get registration from registry + const registration = this.registry.get(name); // Step 4: Handle different types: + switch (registration.type) { + case 'instance': + return registration.instance; + + case 'class': + case 'factory': + if(registration.singleton && registration.instance){ + return registration.instance; + } + + resolutionStack.add(name); + + const dependencies = registration.dependencies.map(depName=>this.resolve(depName, resolutionStack)); + + resolutionStack.delete(name); + + let instance; + if(registration.type === 'class'){ + instance = new registration.Class(...dependencies); + } + else{ + instance = registration.factory(...dependencies); + } + + if(registration.singleton){ + registration.instance = instance; + } + return instance; + + default: + throw new Error('unknown type'); + } // For 'instance': // - Return the stored instance @@ -75,9 +150,6 @@ class Container { // - Remove name from resolutionStack // - If singleton, cache instance // - Return instance - - // Broken: returns undefined (causes test assertions to fail) - return undefined; } /** @@ -87,7 +159,7 @@ class Container { */ has(name) { // TODO: Implement has - throw new Error("Not implemented"); + return this.registry.has(name); } /** @@ -97,7 +169,7 @@ class Container { */ unregister(name) { // TODO: Implement unregister - throw new Error("Not implemented"); + return this.registry.delete(name); } /** @@ -105,7 +177,8 @@ class Container { */ clear() { // TODO: Implement clear - throw new Error("Not implemented"); + this.registry.clear(); + return this; } /** @@ -114,7 +187,7 @@ class Container { */ getRegistrations() { // TODO: Implement getRegistrations - throw new Error("Not implemented"); + return Array.from(this.registry.keys()); } } @@ -133,7 +206,19 @@ function createChildContainer(parent) { const child = new Container(); // Override resolve to check parent... - return child; + const originalResolve = child.resolve.bind(child); + + child.resolve = function(name, resolutionStack = new Set()){ + if(this.registry.has(name)){ + return originalResolve(name, resolutionStack); + } + + if(parent && typeof parent.resolve === 'function'){ + return parent.resolve(name, resolutionStack); + } + + throw new Error(`no registration`); + }; } // Example classes for testing diff --git a/16-state-machine/index.js b/16-state-machine/index.js index d0bad18..fc2ce18 100644 --- a/16-state-machine/index.js +++ b/16-state-machine/index.js @@ -12,11 +12,25 @@ class StateMachine { constructor(config) { // TODO: Implement constructor // Step 1: Validate config has initial and states + + if(!config){ + throw new Error('configuration is required'); + } + if(!config.initial){ + throw new Error('initial state is required'); + } + if(!config.states || typeof config.states !== 'object'){ + throw new Error('states configuration is required'); + } + // Step 2: Store configuration - // this.config = config; - // this.currentState = config.initial; - // this.context = config.context || {}; + this.config = config; + this.currentState = config.initial; + this.context = config.context || {}; // Step 3: Validate initial state exists in states + if (!config.states[config.initial]) { + throw new Error('initial state is not defined in states'); + } } /** @@ -25,7 +39,7 @@ class StateMachine { */ get state() { // TODO: Return current state - throw new Error("Not implemented"); + return this.currentState; } /** @@ -38,24 +52,63 @@ class StateMachine { // TODO: Implement transition // Step 1: Get current state config + const currentStateConfig = this.config.states[this.currentState]; // Step 2: Check if event is valid for current state // Return false if not + if(!currentStateConfig.on || !currentStateConfig.on[event]){ + return false; + } // Step 3: Get transition config (can be string or object) // If string: target = transition // If object: { target, guard, action } + const transitionConfig = currentStateConfig.on[event]; + + let targetState; + let guard = null; + let action = null; + + if(typeof transitionConfig === 'string'){ + targetState = transitionConfig; + } + else if(typeof transitionConfig === 'object'){ + targetState = transitionConfig.target; + guard = transitionConfig.guard; + action = transitionConfig.action; + } + else{ + return false; + } + + if(!this.config.states[targetState]){ + throw new Error('state is not defined'); + } // Step 4: Check guard if present // If guard returns false, return false + if(guard && typeof guard === 'function'){ + const guardResult = guard(this.context, payload); + if(guardResult === false){ + return false; + } + } // Step 5: Update state to target + const previousState = this.currentState; + this.currentState = targetState; // Step 6: Call action if present + if(action && typeof action === 'function'){ + action(this.context, payload,{ + from: previousState, + to: targetState, + event + }); + } // Step 7: Return true - - throw new Error("Not implemented"); + return true; } /** @@ -69,7 +122,20 @@ class StateMachine { // Check if event exists for current state // Check guard if present - throw new Error("Not implemented"); + const currentStateConfig = this.config.states[this.currentState]; + + if(!currentStateConfig.on || !currentStateConfig.on[event]){ + return false; + } + + const transitionConfig = currentStateConfig.on[event]; + + if(typeof transitionConfig === 'object' && transitionConfig.guard){ + const guardResult = transitionConfig.guard(this.context); + return guardResult !== false; + } + + return true; } /** @@ -80,8 +146,13 @@ class StateMachine { // TODO: Implement getAvailableTransitions // Return array of event names from current state's 'on' config + const currentStateConfig = this.config.states[this.currentState]; + + if(!currentStateConfig.on){ + return []; + } - throw new Error("Not implemented"); + return Object.keys(currentStateConfig.on).filter(event => this.can(event)); } /** @@ -90,7 +161,7 @@ class StateMachine { */ getContext() { // TODO: Return context - throw new Error("Not implemented"); + return { ...this.context }; } /** @@ -101,6 +172,16 @@ class StateMachine { // TODO: Implement updateContext // If updater is function: this.context = updater(this.context) // If updater is object: merge with existing context + + if(typeof updater === 'function'){ + this.context = updater(this.context); + } + else if(typeof updater === 'object'){ + this.context = { ...this.context, ...updater }; + } + else{ + throw new Error('must be a function or object'); + } } /** @@ -109,7 +190,10 @@ class StateMachine { */ isFinal() { // TODO: Check if current state has no transitions - throw new Error("Not implemented"); + + const currentStateConfig = this.config.states[this.currentState]; + + return !currentStateConfig.on || Object.keys(currentStateConfig.on).length === 0; } /** @@ -119,6 +203,13 @@ class StateMachine { reset(newContext) { // TODO: Reset to initial state // Optionally reset context + this.currentState = this.config.initial; + if(newContext !== undefined){ + this.context = newContext; + } + else{ + this.context = this.config.context ? { ...this.config.context } : {}; + } } } @@ -134,7 +225,11 @@ function createMachine(config) { // Return a function that creates new StateMachine instances // with the given config - return () => new StateMachine(config); + if(!config || !config.initial || !config.states){ + throw new Error('err'); + } + + return ()=> new StateMachine(config); } module.exports = { StateMachine, createMachine }; diff --git a/17-command-pattern/index.js b/17-command-pattern/index.js index e8cd005..65b6576 100644 --- a/17-command-pattern/index.js +++ b/17-command-pattern/index.js @@ -10,8 +10,8 @@ class CommandManager { constructor() { // TODO: Initialize stacks - // this.undoStack = []; - // this.redoStack = []; + this.undoStack = []; + this.redoStack = []; } /** @@ -21,8 +21,13 @@ class CommandManager { execute(command) { // TODO: Implement execute // Step 1: Call command.execute() + command.execute(); + // Step 2: Push to undo stack + this.undoStack.push(command); + // Step 3: Clear redo stack (new action invalidates redo history) + this.redoStack.length = 0; } /** @@ -33,16 +38,21 @@ class CommandManager { // TODO: Implement undo // Step 1: Check if undo stack is empty + if(this.undoStack.length === 0){ + return false; + } // Step 2: Pop command from undo stack + const command = this.undoStack.pop(); // Step 3: Call command.undo() + command.undo(); // Step 4: Push to redo stack + this.redoStack.push(command); // Step 5: Return true - - throw new Error("Not implemented"); + return true; } /** @@ -53,16 +63,21 @@ class CommandManager { // TODO: Implement redo // Step 1: Check if redo stack is empty + if(this.redoStack.length === 0){ + return false; + } // Step 2: Pop command from redo stack + const command = this.redoStack.pop(); // Step 3: Call command.execute() + command.execute(); // Step 4: Push to undo stack + this.undoStack.push(command); // Step 5: Return true - - throw new Error("Not implemented"); + return true; } /** @@ -71,7 +86,7 @@ class CommandManager { */ canUndo() { // TODO: Return whether undo stack has items - throw new Error("Not implemented"); + return this.undoStack.length > 0; } /** @@ -80,7 +95,7 @@ class CommandManager { */ canRedo() { // TODO: Return whether redo stack has items - throw new Error("Not implemented"); + return this.redoStack.length > 0; } /** @@ -89,7 +104,7 @@ class CommandManager { */ get history() { // TODO: Return copy of undo stack - throw new Error("Not implemented"); + return [...this.undoStack]; } /** @@ -97,6 +112,8 @@ class CommandManager { */ clear() { // TODO: Clear both stacks + this.undoStack.length = 0; + this.redoStack.length = 0; } } @@ -106,17 +123,19 @@ class CommandManager { class AddCommand { constructor(calculator, value) { // TODO: Store calculator and value - // this.calculator = calculator; - // this.value = value; + this.calculator = calculator; + this.value = value; this.description = `Add ${value}`; } execute() { // TODO: Add value to calculator.value + this.calculator.value += this.value; } undo() { // TODO: Subtract value from calculator.value + this.calculator.value -= this.value; } } @@ -126,15 +145,19 @@ class AddCommand { class SubtractCommand { constructor(calculator, value) { // TODO: Store calculator and value + this.calculator = calculator; + this.value = value; this.description = `Subtract ${value}`; } execute() { // TODO: Subtract value from calculator.value + this.calculator.value -= this.value; } undo() { // TODO: Add value to calculator.value + this.calculator.value += this.value; } } @@ -144,16 +167,24 @@ class SubtractCommand { class MultiplyCommand { constructor(calculator, value) { // TODO: Store calculator, value, and previous value for undo + this.calculator = calculator; + this.value = value; + this.previousValue = null; this.description = `Multiply by ${value}`; } execute() { // TODO: Multiply calculator.value by value // Save previous value for undo + this.previousValue = this.calculator.value; + this.calculator.value *= this.value; } undo() { // TODO: Restore previous value + if(this.previousValue !== null){ + this.calculator.value = this.previousValue; + } } } @@ -163,16 +194,29 @@ class MultiplyCommand { class DivideCommand { constructor(calculator, value) { // TODO: Store calculator, value, and previous value for undo + this.calculator = calculator; + this.value = value; + this.previousValue = null; this.description = `Divide by ${value}`; } execute() { // TODO: Divide calculator.value by value // Save previous value for undo + if(this.value === 0){ + throw new Error("cannot divide by zero"); + } + + this.previousValue = this.calculator.value; + + this.calculator.value /= this.value; } undo() { // TODO: Restore previous value + if(this.previousValue !== null){ + this.calculator.value = this.previousValue; + } } } @@ -184,7 +228,7 @@ class DivideCommand { class MacroCommand { constructor(commands = []) { // TODO: Store commands array - // this.commands = commands; + this.commands = commands; this.description = "Macro"; } @@ -194,14 +238,22 @@ class MacroCommand { */ add(command) { // TODO: Add command to array + this.commands.push(command); + this.description = `macro (${this.commands.map(c => c.description).join(", ")})`; } execute() { // TODO: Execute all commands in order + for(const command of this.commands){ + command.execute(); + } } undo() { // TODO: Undo all commands in reverse order + for(let i = this.commands.length - 1; i >= 0; i--){ + this.commands[i].undo(); + } } } @@ -213,15 +265,23 @@ class MacroCommand { class SetValueCommand { constructor(calculator, value) { // TODO: Store calculator, new value, and previous value + this.calculator = calculator; + this.newValue = value; + this.previousValue = null; this.description = `Set to ${value}`; } execute() { // TODO: Save previous, set new value + this.previousValue = this.calculator.value; + this.calculator.value = this.newValue; } undo() { // TODO: Restore previous value + if(this.previousValue !== null){ + this.calculator.value = this.previousValue; + } } } diff --git a/18-strategy-pattern/index.js b/18-strategy-pattern/index.js index f5eee2c..307eb01 100644 --- a/18-strategy-pattern/index.js +++ b/18-strategy-pattern/index.js @@ -14,17 +14,22 @@ class SortContext { constructor(strategy) { // TODO: Store strategy - // this.strategy = strategy; + this.strategy = strategy; } setStrategy(strategy) { // TODO: Update strategy + this.strategy = strategy; } sort(array) { // TODO: Delegate to strategy // Return sorted copy, don't mutate original - throw new Error("Not implemented"); + if(!this.strategy){ + throw new Error('no strategy'); + } + + return this.strategy.sort([...array]); } } @@ -35,8 +40,18 @@ class BubbleSort { sort(array) { // TODO: Implement bubble sort // Return new sorted array - - return ["NOT_IMPLEMENTED"]; // Broken: Replace with implementation + const arr = [...array]; + const n = arr.length; + + for(let i = 0; i < n - 1; i++){ + for(let j = 0; j < n - i - 1; j++){ + if(arr[j] > arr[j + 1]){ + [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; + } + } + } + + return arr; } } @@ -48,7 +63,30 @@ class QuickSort { // TODO: Implement quick sort // Return new sorted array - return []; // Broken: Replace with implementation + const arr = [...array]; + + if(arr.length <= 1){ + return arr; + } + + const pivot = arr[Math.floor(arr.length / 2)]; + const left = []; + const right = []; + const equal = []; + + for(const element of arr){ + if(element < pivot){ + left.push(element); + } + else if (element > pivot){ + right.push(element); + } + else{ + equal.push(element); + } + } + + return [...this.sort(left), ...equal, ...this.sort(right)]; } } @@ -60,7 +98,35 @@ class MergeSort { // TODO: Implement merge sort // Return new sorted array - return []; // Broken: Replace with implementation + const arr = [...array]; + + if(arr.length <= 1){ + return arr; + } + + const mid = Math.floor(arr.length / 2); + const left = this.sort(arr.slice(0, mid)); + const right = this.sort(arr.slice(mid)); + + return this.merge(left, right); + } + + merge(left, right){ + const result = []; + let i = 0, j = 0; + + while(i < left.length && j < right.length){ + if(left[i] < right[j]){ + result.push(left[i]); + i++; + } + else{ + result.push(right[j]); + j++; + } + } + + return [...result, ...left.slice(i), ...right.slice(j)]; } } @@ -76,15 +142,20 @@ class MergeSort { class PricingContext { constructor(strategy) { // TODO: Store strategy + this.strategy = strategy; } setStrategy(strategy) { // TODO: Update strategy + this.strategy = strategy; } calculateTotal(items) { // TODO: Delegate to strategy - throw new Error("Not implemented"); + if(!this.strategy){ + throw new Error('no strategy'); + } + return this.strategy.calculate(items); } } @@ -94,7 +165,7 @@ class PricingContext { class RegularPricing { calculate(items) { // TODO: Sum all item prices - throw new Error("Not implemented"); + return items.reduce((total, item) => total + item.price, 0); } } @@ -104,13 +175,15 @@ class RegularPricing { class PercentageDiscount { constructor(percentage) { // TODO: Store percentage (0-100) - // this.percentage = percentage; + this.percentage = Math.max(0, Math.min(100, percentage)); } calculate(items) { // TODO: Apply percentage discount // total * (1 - percentage/100) - throw new Error("Not implemented"); + const subtotal = items.reduce((total, item) => total + item.price, 0); + const discount = subtotal * (this.percentage / 100); + return Math.max(0, subtotal - discount); } } @@ -120,13 +193,14 @@ class PercentageDiscount { class FixedDiscount { constructor(amount) { // TODO: Store fixed discount amount - // this.amount = amount; + this.amount = Math.max(0, amount); } calculate(items) { // TODO: Subtract fixed amount from total // Don't go below 0 - throw new Error("Not implemented"); + const subtotal = items.reduce((total, item) => total + item.price, 0); + return Math.max(0, subtotal - this.amount); } } @@ -137,7 +211,17 @@ class BuyOneGetOneFree { calculate(items) { // TODO: Every second item is free // Sort by price desc, charge only every other item - throw new Error("Not implemented"); + + const sortedItems = [...items].sort((a, b) => b.price - a.price); + + let total = 0; + for(let i = 0; i < sortedItems.length; i++){ + if(i % 2 === 0){ + total += sortedItems[i].price; + } + } + + return total; } } @@ -151,11 +235,26 @@ class TieredDiscount { // TODO: Store tiers // tiers = [{ threshold: 100, discount: 10 }, { threshold: 200, discount: 20 }] // this.tiers = tiers; + this.tiers = [...tiers].sort((a, b) => a.threshold - b.threshold); } calculate(items) { // TODO: Apply tier discount based on subtotal - throw new Error("Not implemented"); + const subtotal = items.reduce((total, item) => total + item.price, 0); + + let applicableTier = null; + for(const tier of this.tiers){ + if(subtotal >= tier.threshold){ + applicableTier = tier; + } + } + + if(applicableTier){ + const discount = subtotal * (applicableTier.discount / 100); + return subtotal - discount; + } + + return subtotal; } } @@ -169,15 +268,21 @@ class TieredDiscount { class ValidationContext { constructor(strategy) { // TODO: Store strategy + this.strategy = strategy; } setStrategy(strategy) { // TODO: Update strategy + this.strategy = strategy; } validate(data) { // TODO: Delegate to strategy - throw new Error("Not implemented"); + + if(!this.strategy){ + throw new Error('no strategy'); + } + return this.strategy.validate(data); } } @@ -188,7 +293,43 @@ class StrictValidation { validate(data) { // TODO: Strict rules - all fields required, strict format // Return { valid: boolean, errors: string[] } - throw new Error("Not implemented"); + + const errors = []; + + if(!data.name){ + errors.push('name is required'); + } + else if (typeof data.name !== 'string'){ + errors.push('name must be a string'); + } + + if(!data.email){ + errors.push('email is required'); + } + else if(typeof data.email !== 'string'){ + errors.push('email must be a string'); + } + else if(!data.email.includes('@')){ + errors.push('email must be a valid email address'); + } + + if(data.age === undefined || data.age === null){ + errors.push('age is required'); + } + else if(typeof data.age !== 'string' && typeof data.age !== 'number'){ + errors.push('age must be a string or number'); + } + else{ + const ageNum = Number(data.age); + if(isNaN(ageNum) || ageNum < 0){ + errors.push('age must be a valid positive number'); + } + } + + return { + valid: errors.length === 0, + errors + }; } } @@ -198,7 +339,38 @@ class StrictValidation { class LenientValidation { validate(data) { // TODO: Lenient rules - only critical fields required - return { valid: false, errors: ["Not implemented"] }; // Broken: Replace with implementation + + const errors = []; + + if(data.name !== undefined && typeof data.name !== 'string'){ + errors.push('name must be a string if provided'); + } + + if(data.email !== undefined){ + if(typeof data.email !== 'string'){ + errors.push('email must be a string if provided'); + } + else if(!data.email.includes('@')){ + errors.push('email must be a valid email address if provided'); + } + } + + if(data.age !== undefined && data.age !== null){ + if(typeof data.age !== 'string' && typeof data.age !== 'number'){ + errors.push('age must be a string or number if provided'); + } + else{ + const ageNum = Number(data.age); + if(isNaN(ageNum) || ageNum < 0){ + errors.push('age must be a valid positive number if provided'); + } + } + } + + return { + valid: errors.length === 0, + errors + }; } } @@ -214,21 +386,35 @@ class LenientValidation { class StrategyRegistry { constructor() { // TODO: Initialize registry map - // this.strategies = new Map(); + this.strategies = new Map(); } register(name, strategy) { // TODO: Store strategy by name + + if(!name || typeof name !== 'string'){ + throw new Error('no string'); + } + + if(typeof strategy !== 'object' || strategy === null){ + throw new Error('strategy not object'); + } + + this.strategies.set(name, strategy); + return this; } get(name) { // TODO: Return strategy by name - throw new Error("Not implemented"); + if(!this.strategies.has(name)){ + return null; + } + return this.strategies.get(name); } has(name) { // TODO: Check if strategy exists - throw new Error("Not implemented"); + return this.strategies.has(name); } } diff --git a/19-proxy-pattern/index.js b/19-proxy-pattern/index.js index 6d2bf2b..76fd69c 100644 --- a/19-proxy-pattern/index.js +++ b/19-proxy-pattern/index.js @@ -22,17 +22,21 @@ function createValidatingProxy(target, validators) { set(obj, prop, value) { // TODO: Implement set trap // Check validators[prop](value) if validator exists + if(validators && validators[prop]){ + const isValid = validators[prop](value); + if(!isValid){ + throw new Error('failed'); + } + } // Throw if validation fails // Set property if passes - - // Broken: doesn't set at all (fails all tests) + obj[prop] = value; return true; }, get(obj, prop) { // TODO: Implement get trap - // Broken: returns wrong value - return "NOT_IMPLEMENTED"; + return obj[prop]; }, }); } @@ -50,22 +54,32 @@ function createLoggingProxy(target, logger) { return new Proxy(target, { get(obj, prop) { // TODO: Log 'get' and return value - throw new Error("Not implemented"); + const value = obj[prop]; + logger('get', prop, value); + return value; }, set(obj, prop, value) { // TODO: Log 'set' and set value - throw new Error("Not implemented"); + const oldValue = obj[prop]; + obj[prop] = value; + logger('set', prop, value, oldValue); + return true; }, deleteProperty(obj, prop) { // TODO: Log 'delete' and delete property - throw new Error("Not implemented"); + const oldValue = obj[prop]; + const result = delete obj[prop]; + logger('delete', prop, oldValue); + return result; }, has(obj, prop) { // TODO: Log 'has' and return result - throw new Error("Not implemented"); + const result = prop in obj; + logger('has', prop, result); + return result; }, }); } @@ -81,7 +95,7 @@ function createCachingProxy(target, methodNames) { // TODO: Implement caching proxy // Create cache storage - // const cache = new Map(); + const cache = new Map(); return new Proxy(target, { get(obj, prop) { @@ -92,10 +106,24 @@ function createCachingProxy(target, methodNames) { // - Creates cache key from arguments // - Returns cached result if exists // - Otherwise, calls original, caches, and returns + const original = obj[prop]; - // Otherwise, return property normally + if(methodNames.includes(prop) && typeof original === 'function'){ + return function(...args){ + const cacheKey = `${prop}_${JSON.stringify(args)}`; + + if(cache.has(cacheKey)){ + return cache.get(cacheKey); + } - throw new Error("Not implemented"); + const result = original.apply(obj, args); + cache.set(cacheKey, result); + return result; + }; + } + + // Otherwise, return property normally + return original; }, }); } @@ -119,19 +147,30 @@ function createAccessProxy(target, permissions) { // TODO: Check if prop is in readable // Throw if not allowed // Broken: returns wrong value - return "NOT_IMPLEMENTED"; + if(!readable.includes(prop)){ + throw new Error('not allowed'); + } + return obj[prop]; }, set(obj, prop, value) { // TODO: Check if prop is in writable // Throw if not allowed // Broken: doesn't actually set + if(!writable.includes(prop)){ + throw new Error('not allowed'); + } + obj[prop] = value; return true; }, deleteProperty(obj, prop) { // TODO: Only allow if in writable // Broken: doesn't delete + if(!writable.includes(prop)){ + throw new Error('not allowed'); + } + delete obj[prop]; return true; }, }); @@ -156,12 +195,21 @@ function createLazyProxy(loader) { // TODO: Load instance on first access // if (!loaded) { instance = loader(); loaded = true; } // return instance[prop] - throw new Error("Not implemented"); + if(!loaded){ + instance = loader(); + loaded = true; + } + return instance[prop]; }, set(obj, prop, value) { // TODO: Load instance if needed, then set - throw new Error("Not implemented"); + if(!loaded){ + instance = loader(); + loaded = true; + } + instance[prop] = value; + return true; }, }, ); @@ -180,12 +228,21 @@ function createObservableProxy(target, onChange) { return new Proxy(target, { set(obj, prop, value) { // TODO: Call onChange(prop, value, oldValue) on change - throw new Error("Not implemented"); + const oldValue = obj[prop]; + + if(oldValue !== value){ + obj[prop] = value; + onChange(prop, value, oldValue); + } + return true; }, deleteProperty(obj, prop) { // TODO: Call onChange on delete - throw new Error("Not implemented"); + const oldValue = obj[prop]; + delete obj[prop]; + onChange(prop, undefined, oldValue); + return true; }, }); } diff --git a/20-builder-pattern/index.js b/20-builder-pattern/index.js index 69a1e1a..d892f71 100644 --- a/20-builder-pattern/index.js +++ b/20-builder-pattern/index.js @@ -10,11 +10,12 @@ class QueryBuilder { constructor() { // TODO: Initialize state - // this.selectCols = []; - // this.fromTable = null; - // this.whereClauses = []; - // this.orderByClauses = []; - // this.limitCount = null; + this.selectCols = []; + this.fromTable = null; + this.whereClauses = []; + this.orderByClauses = []; + this.limitCount = null; + this.reset(); } /** @@ -24,7 +25,8 @@ class QueryBuilder { */ select(...columns) { // TODO: Store columns - throw new Error("Not implemented"); + this.selectCols = columns.length > 0 ? columns : ['*']; + return this; } /** @@ -34,7 +36,8 @@ class QueryBuilder { */ from(table) { // TODO: Store table name - throw new Error("Not implemented"); + this.fromTable = table; + return this; } /** @@ -46,7 +49,8 @@ class QueryBuilder { */ where(column, operator, value) { // TODO: Store where clause - throw new Error("Not implemented"); + this.whereClauses.push({ column, operator, value }); + return this; } /** @@ -57,7 +61,8 @@ class QueryBuilder { */ orderBy(column, direction = "ASC") { // TODO: Store order by clause - throw new Error("Not implemented"); + this.orderByClauses.push({column, direction: direction.toUpperCase()}); + return this; } /** @@ -67,7 +72,8 @@ class QueryBuilder { */ limit(count) { // TODO: Store limit - throw new Error("Not implemented"); + this.limitCount = count; + return this; } /** @@ -77,7 +83,33 @@ class QueryBuilder { build() { // TODO: Build and return query string // Format: SELECT cols FROM table WHERE clauses ORDER BY clause LIMIT n - throw new Error("Not implemented"); + + if(!this.fromTable){ + throw new Error("no table"); + } + + let query = `SELECT ${this.selectCols.join(', ')} FROM ${this.fromTable}`; + + if(this.whereClauses.length > 0){ + const whereParts = this.whereClauses.map(clause=>{ + const value = typeof clause.value === 'string' ? `'${clause.value}'` : clause.value; + return `${clause.column} ${clause.operator} ${value}`; + }); + query += ` WHERE ${whereParts.join(' AND ')}`; + } + + if(this.orderByClauses.length > 0){ + const orderByParts = this.orderByClauses.map(clause=> + `${clause.column} ${clause.direction}` + ); + query += ` ORDER BY ${orderByParts.join(', ')}`; + } + + if(this.limitCount !== null){ + query += ` LIMIT ${this.limitCount}`; + } + + return query; } /** @@ -86,7 +118,12 @@ class QueryBuilder { */ reset() { // TODO: Reset all state - throw new Error("Not implemented"); + this.selectCols = ['*']; + this.fromTable = null; + this.whereClauses = []; + this.orderByClauses = []; + this.limitCount = null; + return this; } } @@ -98,12 +135,7 @@ class QueryBuilder { class HTMLBuilder { constructor() { // TODO: Initialize state - // this.tagName = 'div'; - // this.idAttr = null; - // this.classes = []; - // this.attributes = {}; - // this.innerContent = ''; - // this.children = []; + this.reset(); } /** @@ -113,7 +145,8 @@ class HTMLBuilder { */ tag(name) { // TODO: Store tag name - throw new Error("Not implemented"); + this.tagName = name; + return this; } /** @@ -123,7 +156,8 @@ class HTMLBuilder { */ id(id) { // TODO: Store id - throw new Error("Not implemented"); + this.idAttr = id; + return this; } /** @@ -133,7 +167,8 @@ class HTMLBuilder { */ class(...classNames) { // TODO: Store classes - throw new Error("Not implemented"); + this.classes.push(...classNames); + return this; } /** @@ -144,7 +179,8 @@ class HTMLBuilder { */ attr(name, value) { // TODO: Store attribute - throw new Error("Not implemented"); + this.attributes[name] = value; + return this; } /** @@ -154,7 +190,8 @@ class HTMLBuilder { */ content(content) { // TODO: Store content - throw new Error("Not implemented"); + this.innerContent = content; + return this; } /** @@ -164,7 +201,8 @@ class HTMLBuilder { */ child(childHtml) { // TODO: Store child - throw new Error("Not implemented"); + this.children.push(childHtml); + return this; } /** @@ -174,7 +212,40 @@ class HTMLBuilder { build() { // TODO: Build and return HTML string // Format: content - throw new Error("Not implemented"); + if(!this.tagName){ + throw new Error("no tag"); + } + + let html = `<${this.tagName}`; + + if(this.idAttr){ + html += ` id="${this.idAttr}"`; + } + + if(this.classes.length > 0){ + html += ` class="${this.classes.join(' ')}"`; + } + + Object.entries(this.attributes).forEach(([name, value])=>{ + html += ` ${name}="${value}"`; + }); + + html += '>'; + + if(this.innerContent){ + html += this.innerContent; + } + + if(this.children.length > 0){ + html += this.children.join(''); + } + + const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']; + if (!voidElements.includes(this.tagName.toLowerCase())){ + html += ``; + } + + return html; } /** @@ -183,7 +254,13 @@ class HTMLBuilder { */ reset() { // TODO: Reset all state - throw new Error("Not implemented"); + this.tagName = 'div'; + this.idAttr = null; + this.classes = []; + this.attributes = {}; + this.innerContent = ''; + this.children = []; + return this; } } @@ -201,6 +278,12 @@ class ConfigBuilder { // features: [], // logLevel: 'info' // }; + this.config = { + environment: 'development', + database: null, + features: [], + logLevel: 'info' + }; } /** @@ -210,7 +293,8 @@ class ConfigBuilder { */ setEnvironment(env) { // TODO: Set environment - throw new Error("Not implemented"); + this.config.environment = env; + return this; } /** @@ -220,7 +304,8 @@ class ConfigBuilder { */ setDatabase(dbConfig) { // TODO: Set database config - throw new Error("Not implemented"); + this.config.database = dbConfig; + return this; } /** @@ -230,7 +315,10 @@ class ConfigBuilder { */ enableFeature(feature) { // TODO: Add feature to list - throw new Error("Not implemented"); + if(!this.config.features.includes(feature)){ + this.config.features.push(feature); + } + return this; } /** @@ -240,7 +328,11 @@ class ConfigBuilder { */ disableFeature(feature) { // TODO: Remove feature from list - throw new Error("Not implemented"); + const index = this.config.features.indexOf(feature); + if(index > -1){ + this.config.features.splice(index, 1); + } + return this; } /** @@ -250,7 +342,8 @@ class ConfigBuilder { */ setLogLevel(level) { // TODO: Set log level - throw new Error("Not implemented"); + this.config.logLevel = level; + return this; } /** @@ -259,7 +352,7 @@ class ConfigBuilder { */ build() { // TODO: Return copy of config - throw new Error("Not implemented"); + return JSON.parse(JSON.stringify(this.config)); } } @@ -271,6 +364,14 @@ class ConfigBuilder { class RequestBuilder { constructor(baseUrl = "") { // TODO: Initialize state + this.baseUrl = baseUrl; + this.urlPath = ''; + this.queryParams = {}; + this.requestConfig = { + method: 'GET', + headers: {}, + body: null + }; } /** @@ -279,7 +380,8 @@ class RequestBuilder { * @returns {RequestBuilder} this */ method(method) { - throw new Error("Not implemented"); + this.requestConfig.method = method.toUpperCase(); + return this; } /** @@ -288,7 +390,8 @@ class RequestBuilder { * @returns {RequestBuilder} this */ path(path) { - throw new Error("Not implemented"); + this.urlPath = path; + return this; } /** @@ -298,7 +401,8 @@ class RequestBuilder { * @returns {RequestBuilder} this */ query(key, value) { - throw new Error("Not implemented"); + this.queryParams[key] = value; + return this; } /** @@ -308,7 +412,8 @@ class RequestBuilder { * @returns {RequestBuilder} this */ header(key, value) { - throw new Error("Not implemented"); + this.requestConfig.headers[key] = value; + return this; } /** @@ -317,7 +422,15 @@ class RequestBuilder { * @returns {RequestBuilder} this */ body(body) { - throw new Error("Not implemented"); + this.requestConfig.body = body; + + if(typeof body === 'object' && body !== null && !Array.isArray(body)){ + if(!this.requestConfig.headers['Content-Type']){ + this.requestConfig.headers['Content-Type'] = 'application/json'; + } + } + + return this; } /** @@ -326,7 +439,24 @@ class RequestBuilder { */ build() { // TODO: Return fetch-compatible config - throw new Error("Not implemented"); + + let url = this.baseUrl; + if(this.urlPath){ + url += this.urlPath; + } + + const queryString = Object.entries(this.queryParams) + .map(([key, value])=>`${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&'); + + if(queryString){ + url += (url.includes('?') ? '&' : '?') + queryString; + } + + return { + url, + ...this.requestConfig + }; } }