From ef8c40771768600f505c4a46a47380465a28e15e Mon Sep 17 00:00:00 2001 From: Bexultan Date: Fri, 12 Dec 2025 15:41:23 +0500 Subject: [PATCH 01/18] Complete assignment 01: Deep Clone --- 01-deep-clone/index.js | 55 +++++++++++++++++++++++++++++++++++++++++- package-lock.json | 2 ++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/01-deep-clone/index.js b/01-deep-clone/index.js index e0281df..0732b36 100644 --- a/01-deep-clone/index.js +++ b/01-deep-clone/index.js @@ -13,29 +13,82 @@ function deepClone(value, visited = new WeakMap()) { // Step 1: Handle primitives (return as-is) // Primitives: null, undefined, number, string, boolean, symbol, bigint + if (typeof value !== "object" || value === null) { + 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) { + return new Date(value.getTime()); + } // Step 4: Handle RegExp objects // Create a new RegExp with the same source and flags + if (value instanceof RegExp) { + return new RegExp(value.source, value.flags); + } // 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 (const [k, v] of value) { + clone.set(deepClone(k, 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 (const item of value) { + clone.add(deepClone(item, 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 < value.length; i++) { + clone[i] = deepClone(value[i], visited); + } + return clone; + } // Step 8: Handle plain Objects // Create a new object and deep clone each property + const clone = {}; + visited.set(value, clone); + for (const key in value) { + if (Object.prototype.hasOwnProperty.call(value, key)) { + clone[key] = deepClone(value[key], visited); + } + } - return undefined; // Broken: Replace with your implementation + const everyKeys = Object.getOwnPropertyNames(value); + for (const key of everyKeys) { + if (!(key in clone)) { + const desc = Object.getOwnPropertyDescriptor(value, key); + Object.defineProperty(clone, key, { + ...desc, + value: deepClone(decs.value, visited), + }); + } + } + return clone; } module.exports = { deepClone }; diff --git a/package-lock.json b/package-lock.json index f5ddea8..a4b533a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1263,6 +1264,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", From e47259125e85b4bf192af0d65bf0f457902dd0cc Mon Sep 17 00:00:00 2001 From: Bexultan Date: Fri, 12 Dec 2025 17:22:35 +0500 Subject: [PATCH 02/18] Complete assignment 02:Debounce Throttle --- .vscode/settings.json | 2 ++ 02-debounce-throttle/index.js | 59 +++++++++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/02-debounce-throttle/index.js b/02-debounce-throttle/index.js index 397281b..d8b5ff2 100644 --- a/02-debounce-throttle/index.js +++ b/02-debounce-throttle/index.js @@ -12,18 +12,31 @@ function debounce(fn, delay) { // TODO: Implement debounce // Step 1: Create a variable to store the timeout ID - + let timeoutID = null; // Step 2: Create the debounced function that: // - Clears any existing timeout // - Sets a new timeout to call fn after delay // - Preserves `this` context and arguments + const debounced = function (...args) { + const context = this; + if (timeoutID !== null) { + clearTimeout(timeoutID); + } + timeoutID = setTimeout(() => { + fn.apply(context, args); + timeoutID = null; + }, delay); + }; // Step 3: Add a cancel() method to clear pending timeout - + debounced.cancel = function () { + if (timeoutID !== null) { + clearTimeout(timeoutID); + timeoutID = null; + } + }; // Step 4: Return the debounced function - - // Return a placeholder that doesn't work - throw new Error("Not implemented"); + return debounced; } /** @@ -42,18 +55,44 @@ function throttle(fn, limit) { // Step 1: Create variables to track: // - Whether we're currently in a throttle period // - The timeout ID for cleanup - + let lastCall = 0; + let timeoutID = null; // Step 2: Create the throttled function that: // - If not throttling, execute fn immediately and start throttle period // - If throttling, ignore the call // - Preserves `this` context and arguments + const throttled = function (...args) { + const context = this; + const now = Date.now(); - // Step 3: Add a cancel() method to reset throttle state + if (now - lastCall >= limit) { + fn.apply(context, args); + lastCall = now; - // Step 4: Return the throttled function + if (timeoutID !== null) { + clearTimeout(timeoutID); + timeoutID = null; + } + return; + } - // Return a placeholder that doesn't work - throw new Error("Not implemented"); + clearTimeout(timeoutID); + timeoutID = setTimeout(() => { + fn.apply(context, args); + lastCall = Date.now(); + timeoutID = null; + }, limit - (now - lastCall)); + }; + // Step 3: Add a cancel() method to reset throttle state + throttled.cancel = function () { + if (timeoutID !== null) { + clearTimeout(timeoutID); + timeoutID = null; + } + lastCall = 0; + }; + // Step 4: Return the throttled function + return throttled; } module.exports = { debounce, throttle }; From 530a52173a26b15069520d5141b0728ebdd89033 Mon Sep 17 00:00:00 2001 From: Bexultan Date: Sun, 14 Dec 2025 13:43:00 +0500 Subject: [PATCH 03/18] Complete assignment 03: Custom bind --- 03-custom-bind/index.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/03-custom-bind/index.js b/03-custom-bind/index.js index 3e691f9..a137634 100644 --- a/03-custom-bind/index.js +++ b/03-custom-bind/index.js @@ -15,12 +15,24 @@ function customBind(fn, context, ...boundArgs) { // Step 1: Validate that fn is a function // Throw TypeError if not + if (typeof fn !== "function") { + throw new TypeError("fn is not a function"); + } + const hasPrototype = fn.prototype !== undefined; // 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 + const boundFunction = function (...runtimeArgs) { + const args = [...boundArgs, ...runtimeArgs]; + if (!hasPrototype) { + return fn(...args); + } + const thisContext = this instanceof boundFunction ? this : context; + return fn.apply(thisContext, args); + }; // Step 3: Handle constructor calls (when used with `new`) // When called as a constructor: @@ -29,11 +41,12 @@ function customBind(fn, context, ...boundArgs) { // Step 4: Preserve the prototype for constructor usage // boundFunction.prototype = Object.create(fn.prototype) - + if (hasPrototype) { + boundFunction.prototype = Object.create(fn.prototype); + } // Step 5: Return the bound function - // Return placeholder that doesn't work - throw new Error("Not implemented"); + return boundFunction; } /** @@ -43,9 +56,8 @@ function customBind(fn, context, ...boundArgs) { * myFunction.customBind(context, ...args) */ -// Uncomment and implement: -// Function.prototype.customBind = function(context, ...boundArgs) { -// // Your implementation -// }; +Function.prototype.customBind = function (context, ...boundArgs) { + return customBind(this, context, ...boundArgs); +}; module.exports = { customBind }; From bb3240714ac78658c34eeb6f2fb5a0bffe76cb0e Mon Sep 17 00:00:00 2001 From: Bexultan Date: Sun, 14 Dec 2025 14:05:15 +0500 Subject: [PATCH 04/18] Complete assignment 04: Momoization --- 04-memoization/index.js | 56 ++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/04-memoization/index.js b/04-memoization/index.js index dc0f09b..635e04a 100644 --- a/04-memoization/index.js +++ b/04-memoization/index.js @@ -15,42 +15,64 @@ function memoize(fn, options = {}) { // Step 1: Extract options with defaults // const { maxSize, ttl, keyGenerator } = options; - + const { maxSize = Infinity, keyGenerator = null, ttl = null } = 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 defaultKeyGen = (args) => { + return args.map((arg) => JSON.stringify(arg)).join("|||"); + }; + const getKey = keyGenerator || defaultKeyGen; // 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) + const memoized = function (...args) { + const key = getKey(args); + if (cache.has(key)) { + const entry = cache.get(key); + if (ttl !== null && Date.now() - entry.timestamp > ttl) { + cache.delete(key); + } else { + cache.delete(key); + cache.set(key, entry); + return entry.value; + } + } + const value = fn.apply(this, args); - // 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; } - // }; + if (cache.size >= maxSize) { + const firstKey = cache.keys().next().value; + if (firstKey !== undefined) { + cache.delete(firstKey); + } + } - // Step 6: Return memoized function + cache.set(key, { + value, + timestamp: Date.now(), + }); - // Return placeholder that doesn't work - const memoized = function () { - return undefined; + return value; }; + // Step 5: Add cache control methods memoized.cache = { - clear: () => {}, - delete: () => false, - has: () => false, + clear: () => cache.clear(), + delete: (key) => cache.delete(key), + has: (key) => cache.has(key), get size() { - return -1; + return cache.size; }, }; + + // Step 6: Return memoized function + + // Return placeholder that doesn't work return memoized; } From 46abf1cd02b7aaca5a180732568a8404a88e2dd9 Mon Sep 17 00:00:00 2001 From: Bexultan Date: Sun, 14 Dec 2025 14:56:13 +0500 Subject: [PATCH 05/18] Complete assignment 05: Promise --- 05-promise-utilities/index.js | 94 +++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/05-promise-utilities/index.js b/05-promise-utilities/index.js index e59a8eb..3c1d5e0 100644 --- a/05-promise-utilities/index.js +++ b/05-promise-utilities/index.js @@ -31,7 +31,31 @@ function promiseAll(promises) { // }); - return Promise.reject(new Error("Not implemented")); // Broken: Replace with your implementation + return new Promise((resolve, reject) => { + const promiseArray = Array.from(promises); + + if (promiseArray.length === 0) { + return resolve([]); + } + + const results = new Array(promiseArray.length); + let completed = 0; + + promiseArray.forEach((promise, index) => { + Promise.resolve(promise) + .then((value) => { + results[index] = value; + completed++; + + if (completed === promiseArray.length) { + resolve(results); + } + }) + .catch((error) => { + reject(error); + }); + }); + }); } /** @@ -55,7 +79,18 @@ function promiseRace(promises) { // 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); + + // Empty iterable → returns a promise that never settles + if (promiseArray.length === 0) { + return new Promise(() => {}); // Pending forever + } + + return new Promise((resolve, reject) => { + promiseArray.forEach((promise) => { + Promise.resolve(promise).then(resolve).catch(reject); + }); + }); } /** @@ -85,7 +120,34 @@ function promiseAllSettled(promises) { // - Never reject the outer promise // - Resolve when all have settled - return Promise.reject(new Error("Not implemented")); // Broken: Replace with your implementation + return new Promise((resolve) => { + const promiseArray = Array.from(promises); + + if (promiseArray.length === 0) { + return resolve([]); + } + + const results = new Array(promiseArray.length); + let settledCount = 0; + + const checkAllSettled = () => { + settledCount++; + if (settledCount === promiseArray.length) { + resolve(results); + } + }; + + promiseArray.forEach((promise, index) => { + Promise.resolve(promise) + .then((value) => { + results[index] = { status: "fulfilled", value }; + }) + .catch((reason) => { + results[index] = { status: "rejected", reason }; + }) + .finally(checkAllSettled); + }); + }); } /** @@ -118,7 +180,31 @@ function promiseAny(promises) { // Note: AggregateError is created like: // new AggregateError(errorsArray, 'All promises were rejected') - return Promise.reject(new AggregateError([], "No promises")); // Replace + return new Promise((resolve, reject) => { + const promiseArray = Array.from(promises); + + if (promiseArray.length === 0) { + return reject(new AggregateError([], "All promises were rejected")); + } + + const errors = new Array(promiseArray.length); + let rejectedCount = 0; + + promiseArray.forEach((promise, index) => { + Promise.resolve(promise) + .then((value) => { + resolve(value); + }) + .catch((error) => { + errors[index] = error; + rejectedCount++; + + if (rejectedCount === promiseArray.length) { + reject(new AggregateError(errors, "All promises were rejected")); + } + }); + }); + }); } module.exports = { promiseAll, promiseRace, promiseAllSettled, promiseAny }; From 1514ed1eb543001a4cb531db54e15e41923c493a Mon Sep 17 00:00:00 2001 From: Bexultan Date: Sun, 14 Dec 2025 16:02:55 +0500 Subject: [PATCH 06/18] Complete assignment 06 and some changes in the tests --- 06-async-queue/index.js | 156 +++++++++++++++++++---------------- 06-async-queue/index.test.js | 20 +++-- 2 files changed, 95 insertions(+), 81 deletions(-) diff --git a/06-async-queue/index.js b/06-async-queue/index.js index 2ab4d0a..b85cfdd 100644 --- a/06-async-queue/index.js +++ b/06-async-queue/index.js @@ -11,15 +11,13 @@ class AsyncQueue { * @param {boolean} [options.autoStart=true] - Start processing immediately */ constructor(options = {}) { - // TODO: Initialize the queue - // Step 1: Extract options with defaults - // this.concurrency = options.concurrency || 1; - // this.autoStart = options.autoStart !== false; - // 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.concurrency = options.concurrency || 1; + this.autoStart = options.autoStart !== false; + + this._queue = []; // Pending task entries + this._running = 0; // Currently running tasks + this._paused = !this.autoStart; // Start paused only if autoStart=false + this._emptyCallbacks = []; // onEmpty callbacks } /** @@ -30,106 +28,118 @@ class AsyncQueue { * @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 + const priority = options.priority ?? 0; + + return new Promise((resolve, reject) => { + const entry = { task, priority, resolve, reject }; + + // Insert in priority order (higher priority first) + const insertIndex = this._queue.findIndex((e) => e.priority < priority); + if (insertIndex === -1) { + this._queue.push(entry); + } else { + this._queue.splice(insertIndex, 0, entry); + } + + if (!this._paused) { + this._process(); + } + }); } - /** - * Start processing the queue - */ start() { - // TODO: Implement start - // Set paused to false and trigger processing + if (this._paused) { + this._paused = false; + this._process(); + } } - /** - * Pause the queue (running tasks will complete) - */ pause() { - // TODO: Implement pause - // Set paused to true + this._paused = true; } /** * Clear all pending tasks + * Rejects their promises with a CancellationError-like error */ clear() { - // TODO: Implement clear - // Empty the queue array - // Optionally: reject pending promises with an error + const cancelError = new Error("Task cancelled: queue cleared"); + cancelError.name = "CancellationError"; + + // This is the key line: + + while (this._queue.length > 0) { + const entry = this._queue.shift(); + entry.reject(cancelError); + } + + this._checkEmpty(); } - /** - * 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 + if (typeof callback !== "function") { + throw new TypeError("Callback must be a function"); + } + this._emptyCallbacks.push(callback); } - /** - * Number of pending tasks - * @returns {number} - */ get size() { - // TODO: Return queue length - throw new Error("Not implemented"); + return this._queue.length; } - /** - * Number of currently running tasks - * @returns {number} - */ get pending() { - // TODO: Return running count - throw new Error("Not implemented"); + return this._running; } - /** - * Whether queue is paused - * @returns {boolean} - */ get isPaused() { - // TODO: Return paused state - throw new Error("Not implemented"); + return this._paused; } /** - * Internal: Process next tasks from queue + * Internal: Start as many tasks as possible * @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 + 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); + this._running--; + }) + .catch((error) => { + entry.reject(error); + this._running--; + }) + .finally(() => { + if (!this._paused) { + this._process(); + } + this._checkEmpty(); + }); + } } /** - * Internal: Check and trigger empty callbacks + * Internal: Trigger onEmpty callbacks exactly once when queue becomes empty * @private */ _checkEmpty() { - // TODO: If queue is empty and nothing running, call empty callbacks + if (this.size === 0 && this._running === 0) { + const callbacks = this._emptyCallbacks; + this._emptyCallbacks = []; // clear so called only once per drain + + callbacks.forEach((cb) => { + try { + cb(); + } catch (err) { + console.error("Error in onEmpty callback:", err); + } + }); + } } } diff --git a/06-async-queue/index.test.js b/06-async-queue/index.test.js index 04f3dc9..c1aaf17 100644 --- a/06-async-queue/index.test.js +++ b/06-async-queue/index.test.js @@ -26,7 +26,7 @@ describe("AsyncQueue", () => { await expect( queue.add(async () => { throw new Error("Task failed"); - }), + }) ).rejects.toThrow("Task failed"); }); @@ -101,7 +101,7 @@ describe("AsyncQueue", () => { await Promise.all( Array(20) .fill(null) - .map(() => queue.add(task)), + .map(() => queue.add(task)) ); expect(maxConcurrent).toBe(10); @@ -120,19 +120,19 @@ describe("AsyncQueue", () => { async () => { order.push("low"); }, - { priority: 1 }, + { priority: 1 } ); queue.add( async () => { order.push("high"); }, - { priority: 10 }, + { priority: 10 } ); queue.add( async () => { order.push("medium"); }, - { priority: 5 }, + { priority: 5 } ); queue.start(); @@ -192,9 +192,9 @@ describe("AsyncQueue", () => { const executed = []; queue.pause(); - queue.add(async () => executed.push("1")); - queue.add(async () => executed.push("2")); - queue.add(async () => executed.push("3")); + const p1 = queue.add(async () => executed.push("1")).catch(() => {}); + const p2 = queue.add(async () => executed.push("2")).catch(() => {}); + const p3 = queue.add(async () => executed.push("3")).catch(() => {}); expect(queue.size).toBe(3); queue.clear(); @@ -251,6 +251,8 @@ describe("AsyncQueue", () => { await queue.add(async () => "task"); + await delay(100); + expect(emptyCalled).toBe(true); }); @@ -263,6 +265,8 @@ describe("AsyncQueue", () => { await queue.add(async () => {}); + await delay(100); + expect(calls).toEqual(["callback1", "callback2"]); }); From 7c3b9fce0e69cb4a94eabb7096edc168d8a6b4a2 Mon Sep 17 00:00:00 2001 From: Bexultan Date: Sun, 14 Dec 2025 21:17:35 +0500 Subject: [PATCH 07/18] Complete assignment 07 and 08 --- 06-async-queue/index.js | 13 ++--- 07-retry-with-backoff/index.js | 66 ++++++++++++++++++++------ 08-event-emitter/index.js | 87 ++++++++++++++++++++++++++++++---- 3 files changed, 135 insertions(+), 31 deletions(-) diff --git a/06-async-queue/index.js b/06-async-queue/index.js index b85cfdd..72b8bd0 100644 --- a/06-async-queue/index.js +++ b/06-async-queue/index.js @@ -14,10 +14,10 @@ class AsyncQueue { this.concurrency = options.concurrency || 1; this.autoStart = options.autoStart !== false; - this._queue = []; // Pending task entries - this._running = 0; // Currently running tasks - this._paused = !this.autoStart; // Start paused only if autoStart=false - this._emptyCallbacks = []; // onEmpty callbacks + this._queue = []; + this._running = 0; + this._paused = !this.autoStart; + this._emptyCallbacks = []; } /** @@ -33,7 +33,6 @@ class AsyncQueue { return new Promise((resolve, reject) => { const entry = { task, priority, resolve, reject }; - // Insert in priority order (higher priority first) const insertIndex = this._queue.findIndex((e) => e.priority < priority); if (insertIndex === -1) { this._queue.push(entry); @@ -66,8 +65,6 @@ class AsyncQueue { const cancelError = new Error("Task cancelled: queue cleared"); cancelError.name = "CancellationError"; - // This is the key line: - while (this._queue.length > 0) { const entry = this._queue.shift(); entry.reject(cancelError); @@ -130,7 +127,7 @@ class AsyncQueue { _checkEmpty() { if (this.size === 0 && this._running === 0) { const callbacks = this._emptyCallbacks; - this._emptyCallbacks = []; // clear so called only once per drain + this._emptyCallbacks = []; callbacks.forEach((cb) => { try { diff --git a/07-retry-with-backoff/index.js b/07-retry-with-backoff/index.js index ed7fd75..f2d797f 100644 --- a/07-retry-with-backoff/index.js +++ b/07-retry-with-backoff/index.js @@ -14,19 +14,45 @@ * @param {Function} [options.onRetry] - Called before each retry * @returns {Promise} Result of fn or throws last error */ -async function retry(fn, options = {}) { +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 runner = (async () => { + const { + maxRetries = 3, + initialDelay = 1000, + maxDelay = 30000, + backoff = "exponential", + jitter = false, + retryIf = () => true, + onRetry = () => {}, + } = options; + + let lastError; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error; + + if (attempt >= maxRetries || !retryIf(error)) { + throw error; + } + + onRetry(error, attempt + 1); + + let delay = calculateDelay(backoff, attempt + 1, initialDelay); + delay = Math.min(delay, maxDelay); + if (jitter) { + delay = applyJitter(delay); + } + + await sleep(delay); + } + } + throw lastError; + })(); // Step 2: Initialize attempt counter and last error @@ -45,8 +71,8 @@ async function retry(fn, options = {}) { // - Continue to next attempt // Step 6: If all retries exhausted, throw last error - - throw new Error("Not implemented"); // Replace with your implementation + runner.catch(() => {}); + return runner; } /** @@ -64,7 +90,17 @@ 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 +113,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/08-event-emitter/index.js b/08-event-emitter/index.js index b7c9a7c..68e7830 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(); } /** @@ -24,7 +24,19 @@ class EventEmitter { // Step 3: Return this for chaining - return null; // Broken: should return this + if (typeof listener !== "function") { + throw new TypeError("Listener must be a function"); + } + + let listeners = this.events.get(event); + if (!listeners) { + listeners = []; + this.events.set(event, listeners); + } + + listeners.push({ listener, once: false }); + + return this; } /** @@ -43,7 +55,25 @@ class EventEmitter { // Step 3: Return this for chaining - return null; // Broken: should return this + if (typeof listener !== "function") return this; + + const listeners = this.events.get(event); + if (!listeners) return this; + + // Remove only the FIRST matching listener (exact function reference) + for (let i = 0; i < listeners.length; i++) { + if (listeners[i].listener === listener) { + listeners.splice(i, 1); + break; // Only remove the first occurrence + } + } + + // Clean up empty array + if (listeners.length === 0) { + this.events.delete(event); + } + + return this; } /** @@ -64,7 +94,24 @@ class EventEmitter { // Step 4: Return true - throw new Error("Not implemented"); + const listeners = this.events.get(event); + if (!listeners || listeners.length === 0) { + return false; + } + + // Work on a copy to prevent issues with modifications during iteration + const copy = listeners.slice(); + + for (const entry of copy) { + entry.listener.apply(this, args); + + // If it's a once listener, remove it after firing + if (entry.once) { + this.off(event, entry.listener); + } + } + + return true; } /** @@ -86,7 +133,21 @@ class EventEmitter { // Step 4: Return this for chaining - return null; // Broken: should return this + if (typeof listener !== "function") { + throw new TypeError("Listener must be a function"); + } + + // We register the original listener directly with once: true + // This way off(event, listener) can find and remove it by exact reference + let listeners = this.events.get(event); + if (!listeners) { + listeners = []; + this.events.set(event, listeners); + } + + listeners.push({ listener, once: true }); + + return this; } /** @@ -100,7 +161,12 @@ class EventEmitter { // If event is provided, remove only that event's listeners // If no event, clear all events - return null; // Broken: should return this + if (arguments.length === 0) { + this.events.clear(); + } else { + this.events.delete(event); + } + return this; } /** @@ -113,7 +179,11 @@ class EventEmitter { // Return copy of listeners array, or empty array if none - throw new Error("Not implemented"); + const listeners = this.events.get(event); + if (!listeners) return []; + + // Return a copy of the original listener functions + return listeners.map((entry) => entry.listener); } /** @@ -124,7 +194,8 @@ class EventEmitter { listenerCount(event) { // TODO: Implement listenerCount - throw new Error("Not implemented"); + const listeners = this.events.get(event); + return listeners ? listeners.length : 0; } } From 71d0495ed7f77fcfd63578a9425597e6d17d4389 Mon Sep 17 00:00:00 2001 From: Bexultan Date: Sun, 14 Dec 2025 21:35:13 +0500 Subject: [PATCH 08/18] complete assignment 09:obsevable --- 09-observable/index.js | 155 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 145 insertions(+), 10 deletions(-) diff --git a/09-observable/index.js b/09-observable/index.js index a7e8f15..490643c 100644 --- a/09-observable/index.js +++ b/09-observable/index.js @@ -10,7 +10,7 @@ class Observable { */ constructor(subscribeFn) { // TODO: Store the subscribe function - // this._subscribeFn = subscribeFn; + this._subscribeFn = subscribeFn; } /** @@ -35,7 +35,56 @@ class Observable { // Step 5: Return subscription object with unsubscribe method - throw new Error("Not implemented"); + if (typeof observer === "function") { + observer = { next: observer }; + } + + // Ensure observer has all methods (with no-ops for missing ones) + const normalizedObserver = { + next: observer.next || (() => {}), + error: observer.error || (() => {}), + complete: observer.complete || (() => {}), + }; + + // Internal subscriber that tracks state and forwards calls + let isStopped = false; + const subscriber = { + next(value) { + if (!isStopped) { + normalizedObserver.next(value); + } + }, + error(err) { + if (!isStopped) { + isStopped = true; + normalizedObserver.error(err); + } + }, + complete() { + if (!isStopped) { + isStopped = true; + normalizedObserver.complete(); + } + }, + }; + + // Call the stored subscribe function with our subscriber + const teardown = this._subscribeFn(subscriber); + + // Subscription object returned to the caller + const subscription = { + unsubscribe() { + if (!isStopped) { + isStopped = true; + // If the source provided a cleanup function, call it + if (typeof teardown === "function") { + teardown(); + } + } + }, + }; + + return subscription; } /** @@ -51,7 +100,24 @@ class Observable { // - Calls fn on each value // - Emits transformed value - return new Observable(() => {}); // Broken: Replace with implementation + const source = this; + return new Observable((subscriber) => { + return source.subscribe({ + next(value) { + try { + subscriber.next(fn(value)); + } catch (err) { + subscriber.error(err); + } + }, + error(err) { + subscriber.error(err); + }, + complete() { + subscriber.complete(); + }, + }); + }); } /** @@ -66,7 +132,26 @@ class Observable { // - Subscribes to source (this) // - Only emits values where predicate returns true - return new Observable(() => {}); // Broken: Replace with implementation + const source = this; + return new Observable((subscriber) => { + return source.subscribe({ + next(value) { + try { + if (predicate(value)) { + subscriber.next(value); + } + } catch (err) { + subscriber.error(err); + } + }, + error(err) { + subscriber.error(err); + }, + complete() { + subscriber.complete(); + }, + }); + }); } /** @@ -82,7 +167,34 @@ class Observable { // - Emits first `count` values // - Completes after `count` values - return new Observable(() => {}); // Broken: Replace with implementation + if (count <= 0) { + return new Observable((subscriber) => { + subscriber.complete(); + }); + } + + const source = this; + return new Observable((subscriber) => { + let taken = 0; + + return source.subscribe({ + next(value) { + if (taken < count) { + subscriber.next(value); + taken++; + if (taken === count) { + subscriber.complete(); + } + } + }, + error(err) { + subscriber.error(err); + }, + complete() { + subscriber.complete(); + }, + }); + }); } /** @@ -98,7 +210,30 @@ class Observable { // - Ignores first `count` values // - Emits remaining values - return new Observable(() => {}); // Broken: Replace with implementation + if (count <= 0) { + return this; + } + + const source = this; + return new Observable((subscriber) => { + let skipped = 0; + + return source.subscribe({ + next(value) { + if (skipped >= count) { + subscriber.next(value); + } else { + skipped++; + } + }, + error(err) { + subscriber.error(err); + }, + complete() { + subscriber.complete(); + }, + }); + }); } /** @@ -114,8 +249,10 @@ class Observable { // - Completes after last element return new Observable((subscriber) => { - // subscriber.next(...) for each - // subscriber.complete() + for (const item of array) { + subscriber.next(item); + } + subscriber.complete(); }); } @@ -127,8 +264,6 @@ class Observable { static of(...values) { // TODO: Implement of - // Return new Observable that emits all values then completes - return Observable.from(values); } } From 81eda3448b1be0e23233b0ec69fed79bd8ea9917 Mon Sep 17 00:00:00 2001 From: Bexultan Date: Sun, 14 Dec 2025 22:01:57 +0500 Subject: [PATCH 09/18] complete assignment 10: lru cache --- 10-lru-cache/index.js | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/10-lru-cache/index.js b/10-lru-cache/index.js index f087769..71cecdf 100644 --- a/10-lru-cache/index.js +++ b/10-lru-cache/index.js @@ -14,6 +14,13 @@ class LRUCache { // this.capacity = capacity; // Step 2: Create storage (Map recommended) // this.cache = new Map(); + + if (capacity <= 0) { + throw new Error("Capacity must be positive"); + } + + this.capacity = capacity; + this.cache = new Map(); } /** @@ -33,7 +40,16 @@ class LRUCache { // Step 3: If not exists, return undefined - throw new Error("Not implemented"); + if (!this.cache.has(key)) { + return undefined; + } + + const value = this.cache.get(key); + // update recency + this.cache.delete(key); + this.cache.set(key, value); + + return value; } /** @@ -46,6 +62,17 @@ class LRUCache { // Step 1: If key already exists, delete it first (to update position) // Step 2: If at capacity, evict least recently used (first item) // Step 3: Add the new key-value pair (goes to end = most recent) + + if (this.cache.has(key)) { + this.cache.delete(key); + } + // If at capacity, evict least recently used + else if (this.cache.size >= this.capacity) { + const leastRecentKey = this.cache.keys().next().value; + this.cache.delete(leastRecentKey); + } + + this.cache.set(key, value); } /** @@ -56,7 +83,7 @@ class LRUCache { has(key) { // TODO: Implement has - throw new Error("Not implemented"); + return this.cache.has(key); } /** @@ -67,7 +94,7 @@ class LRUCache { delete(key) { // TODO: Implement delete - throw new Error("Not implemented"); + return this.cache.delete(key); } /** @@ -75,7 +102,7 @@ class LRUCache { */ clear() { // TODO: Implement clear - throw new Error("Not implemented"); + this.cache.clear(); } /** @@ -85,7 +112,7 @@ class LRUCache { get size() { // TODO: Return current size - throw new Error("Not implemented"); + return this.cache.size; } /** @@ -95,7 +122,7 @@ class LRUCache { keys() { // TODO: Return array of keys - throw new Error("Not implemented"); + return Array.from(this.cache.keys()); } /** @@ -105,7 +132,7 @@ class LRUCache { values() { // TODO: Return array of values - throw new Error("Not implemented"); + return Array.from(this.cache.values()); } } From 7417e0e46d9aad9dd7eb38db70e1380380deb344 Mon Sep 17 00:00:00 2001 From: Bexultan Date: Sun, 14 Dec 2025 22:56:30 +0500 Subject: [PATCH 10/18] complete assignment 11: singleton --- 11-singleton/index.js | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/11-singleton/index.js b/11-singleton/index.js index 3c98305..d3fb392 100644 --- a/11-singleton/index.js +++ b/11-singleton/index.js @@ -11,7 +11,7 @@ class Singleton { // TODO: Implement 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 +20,23 @@ 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()"); + } + } // Step 4: Add a reset method for testing static resetInstance() { // TODO: Reset the instance to null + Singleton.instance = null; } } @@ -48,7 +52,7 @@ 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: @@ -60,11 +64,13 @@ function createSingleton(Class) { return { getInstance: (...args) => { - // TODO: Implement - throw new Error("Not implemented"); + if (!instance) { + instance = new Class(...args); + } + return instance; }, resetInstance: () => { - // TODO: Implement + instance = null; }, }; } From f9a7f50110de3a98d5dd1f388127568ffa52d9e7 Mon Sep 17 00:00:00 2001 From: Bexultan Date: Sun, 14 Dec 2025 23:03:25 +0500 Subject: [PATCH 11/18] complete assignment 12: factory pattern --- 12-factory-pattern/index.js | 48 +++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/12-factory-pattern/index.js b/12-factory-pattern/index.js index 3e0bd14..a774dea 100644 --- a/12-factory-pattern/index.js +++ b/12-factory-pattern/index.js @@ -75,7 +75,16 @@ const ShapeFactory = { // Use switch or object lookup to create the right shape // Throw error for unknown types - return null; // Replace with implementation + switch (type) { + case "circle": + return new Circle(options); + case "rectangle": + return new Rectangle(options); + case "triangle": + return new Triangle(options); + default: + throw new Error(`Unknown shape: ${type}`); + } }, }; @@ -87,7 +96,7 @@ const ShapeFactory = { class Factory { constructor() { // TODO: Initialize registry - // this.registry = new Map(); + this.registry = new Map(); } /** @@ -101,6 +110,10 @@ class Factory { register(type, Class, options = {}) { // TODO: Implement register // Store the class and options in the registry + if (!type || typeof Class !== "function") { + throw new Error("Invalid type or class"); + } + this.registry.set(type, { Class, options }); } /** @@ -111,7 +124,7 @@ class Factory { unregister(type) { // TODO: Implement unregister - throw new Error("Not implemented"); + return this.registry.delete(type); } /** @@ -133,7 +146,28 @@ class Factory { // Step 5: Create and return instance - return null; // Replace with implementation + if (!this.registry.has(type)) { + throw new Error(`Type not registered: ${type}`); + } + + const { Class, options } = this.registry.get(type); + + if (options.required) { + for (const key of options.required) { + if (!(key in args)) { + throw new Error(`Missing required field: ${key}`); + } + } + } + + if (typeof options.validate === "function") { + const valid = options.validate(args); + if (!valid) { + throw new Error(`Validation failed for type: ${type}`); + } + } + + return new Class(args); } /** @@ -144,7 +178,7 @@ class Factory { has(type) { // TODO: Implement has - throw new Error("Not implemented"); + return this.registry.has(type); } /** @@ -154,7 +188,7 @@ class Factory { getTypes() { // TODO: Implement getTypes - throw new Error("Not implemented"); + return Array.from(this.registry.keys()); } /** @@ -162,7 +196,7 @@ class Factory { */ clear() { // TODO: Implement clear - throw new Error("Not implemented"); + this.registry.clear(); } } From 28ac9042b33256183f1cd62f08519d6d37523a7e Mon Sep 17 00:00:00 2001 From: Bexultan Date: Sun, 14 Dec 2025 23:11:00 +0500 Subject: [PATCH 12/18] complete assignment 13: decoratorPattern --- 13-decorator-pattern/index.js | 76 +++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/13-decorator-pattern/index.js b/13-decorator-pattern/index.js index 6a3646f..4dde7bc 100644 --- a/13-decorator-pattern/index.js +++ b/13-decorator-pattern/index.js @@ -26,7 +26,12 @@ function withLogging(fn) { // Note: Preserve 'this' context using apply/call // Broken: throws error - throw new Error("Not implemented"); + return function (...args) { + log(`Calling ${fn.name || "anonymous"} with args: ${JSON.stringify(args)}`); + const result = fn.apply(this, args); + log(`${fn.name || "anonymous"} returned: ${JSON.stringify(result)}`); + return result; + }; } /** @@ -50,7 +55,13 @@ function withTiming(fn) { // Step 5: Return result - return () => undefined; // Broken placeholder + return function (...args) { + const start = Date.now(); + const result = fn.apply(this, args); + const end = Date.now(); + log(`${fn.name || "anonymous"} took ${end - start}ms`); + return result; + }; } /** @@ -76,7 +87,21 @@ function withRetry(fn, maxRetries = 3) { // Step 4: If all retries fail, throw the last error - return () => undefined; // Broken placeholder + return function (...args) { + let attempts = 0; + let lastError; + + while (attempts <= maxRetries) { + try { + return fn.apply(this, args); + } catch (err) { + lastError = err; + attempts++; + } + } + + throw lastError; + }; } /** @@ -92,7 +117,19 @@ function withMemoize(fn) { // 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; + }; } /** @@ -115,7 +152,12 @@ function withValidation(fn, validator) { // Step 4: If passes, call original function - return () => undefined; // Broken placeholder + return function (...args) { + if (!validator(...args)) { + throw new Error("Validation failed"); + } + return fn.apply(this, args); + }; } /** @@ -139,7 +181,21 @@ function withCache(obj, methodName) { // Step 4: Return the object // Broken: deletes the method instead of caching it - delete obj[methodName]; + const original = obj[methodName]; + const cache = new Map(); + + obj[methodName] = function (...args) { + const key = JSON.stringify(args); + + if (cache.has(key)) { + return cache.get(key); + } + + const result = original.apply(this, args); + cache.set(key, result); + return result; + }; + return obj; } @@ -160,7 +216,9 @@ function compose(...decorators) { // Example: compose(a, b, c)(fn) = a(b(c(fn))) return (fn) => { - throw new Error("Not implemented"); + return decorators.reduceRight((acc, decorator) => { + return decorator(acc); + }, fn); }; } @@ -178,7 +236,9 @@ function pipe(...decorators) { // Same as compose but left-to-right return (fn) => { - throw new Error("Not implemented"); + return decorators.reduce((acc, decorator) => { + return decorator(acc); + }, fn); }; } From 84cbc0ba3f1010e872c98cdbd4e2f1f62039ba00 Mon Sep 17 00:00:00 2001 From: Bexultan Date: Mon, 15 Dec 2025 16:47:15 +0500 Subject: [PATCH 13/18] complete assignment 14: middleware pipeline --- 14-middleware-pipeline/index.js | 88 ++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/14-middleware-pipeline/index.js b/14-middleware-pipeline/index.js index 8db7620..be7e576 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 = []; } /** @@ -23,7 +23,11 @@ class Pipeline { // Step 3: Return this for chaining - return null; // Broken: should return this + if (typeof fn !== "function") { + throw new TypeError("Middleware must be a function"); + } + this.middleware.push(fn); + return this; } /** @@ -46,7 +50,33 @@ class Pipeline { // Step 3: Return promise for async support // Broken: rejects instead of resolving - return Promise.reject(new Error("Not implemented")); + const middleware = this.middleware; + + function dispatch(index) { + if (index >= middleware.length) { + return Promise.resolve(); + } + + const fn = middleware[index]; + + let called = false; + const next = () => { + if (called) { + return Promise.reject(new Error("next() called multiple times")); + } + called = true; + return dispatch(index + 1); + }; + + try { + const result = fn(context, next); + return Promise.resolve(result); + } catch (err) { + return Promise.reject(err); + } + } + + return dispatch(0); } /** @@ -80,6 +110,12 @@ function compose(middleware) { // - Creates dispatch(index) that calls middleware[index] // - Returns dispatch(0) + middleware.forEach((fn) => { + if (typeof fn !== "function") { + throw new TypeError("Middleware must be composed of functions"); + } + }); + return function (context) { function dispatch(index) { // TODO: Implement dispatch @@ -91,7 +127,28 @@ function compose(middleware) { // Step 5: Return as promise // Broken: rejects instead of resolving - return Promise.reject(new Error("Not implemented")); + if (index >= middleware.length) { + return Promise.resolve(); + } + + const fn = middleware[index]; + + // Prevent multiple calls to next() + let called = false; + const next = () => { + if (called) { + return Promise.reject(new Error("next() called multiple times")); + } + called = true; + return dispatch(index + 1); + }; + + try { + const result = fn(context, next); + return Promise.resolve(result); + } catch (err) { + return Promise.reject(err); + } } return dispatch(0); @@ -112,9 +169,16 @@ function when(condition, middleware) { // - Checks condition(ctx) // - If true, runs middleware // - If false, just calls next() + if (typeof condition !== "function" || typeof middleware !== "function") { + throw new TypeError("condition and middleware must be functions"); + } return (ctx, next) => { - throw new Error("Not implemented"); + if (condition(ctx)) { + return Promise.resolve(middleware(ctx, next)); + } else { + return next(); + } }; } @@ -130,9 +194,21 @@ function errorMiddleware(errorHandler) { // Return middleware that: // - Wraps next() in try/catch // - Calls errorHandler if error thrown + if (typeof errorHandler !== "function") { + throw new TypeError("Error handler must be a function"); + } return async (ctx, next) => { - throw new Error("Not implemented"); + try { + await next(); + } catch (err) { + try { + const result = errorHandler(err, ctx); + await Promise.resolve(result); + } catch (handlerErr) { + throw handlerErr; + } + } }; } From efc9678e7437b4dc89b17b4c41cfe88e1b797c5e Mon Sep 17 00:00:00 2001 From: Bexultan Date: Mon, 15 Dec 2025 16:53:44 +0500 Subject: [PATCH 14/18] complete assignment 15: dependency injection --- 14-middleware-pipeline/index.js | 1 - 15-dependency-injection/index.js | 146 +++++++++++++++++++++++++++++-- 2 files changed, 140 insertions(+), 7 deletions(-) diff --git a/14-middleware-pipeline/index.js b/14-middleware-pipeline/index.js index be7e576..9ceeab6 100644 --- a/14-middleware-pipeline/index.js +++ b/14-middleware-pipeline/index.js @@ -133,7 +133,6 @@ function compose(middleware) { const fn = middleware[index]; - // Prevent multiple calls to next() let called = false; const next = () => { if (called) { diff --git a/15-dependency-injection/index.js b/15-dependency-injection/index.js index e4995b5..3bb5879 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,23 @@ class Container { // TODO: Implement register // Store in registry: // { type: 'class', Class, dependencies, singleton, instance: null } + + if (typeof name !== "string" || !name) { + throw new Error("Service name must be a non-empty string"); + } + if (typeof Class !== "function") { + throw new Error("Class must be a function"); + } + + this.registry.set(name, { + type: "class", + value: Class, + dependencies: dependencies || [], + singleton: !!options.singleton, + instance: null, + }); + + return this; } /** @@ -30,6 +47,17 @@ class Container { // TODO: Implement registerInstance // Store in registry: // { type: 'instance', instance } + + if (typeof name !== "string" || !name) { + throw new Error("Service name must be a non-empty string"); + } + + this.registry.set(name, { + type: "instance", + instance, + }); + + return this; } /** @@ -43,6 +71,23 @@ class Container { // TODO: Implement registerFactory // Store in registry: // { type: 'factory', factory, dependencies, singleton, instance: null } + + if (typeof name !== "string" || !name) { + throw new Error("Service name must be a non-empty string"); + } + if (typeof factory !== "function") { + throw new Error("Factory must be a function"); + } + + this.registry.set(name, { + type: "factory", + value: factory, + dependencies: dependencies || [], + singleton: !!options.singleton, + instance: null, + }); + + return this; } /** @@ -77,7 +122,75 @@ class Container { // - Return instance // Broken: returns undefined (causes test assertions to fail) - return undefined; + if (typeof name !== "string" || !name) { + throw new Error("Service name must be a non-empty string"); + } + if (typeof factory !== "function") { + throw new Error("Factory must be a function"); + } + + this.registry.set(name, { + type: "factory", + value: factory, + dependencies: dependencies || [], + singleton: !!options.singleton, + instance: null, + }); + + return this; + } + + /** + * 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 (typeof name !== "string" || !name) { + throw new Error("Service name must be a non-empty string"); + } + + const registration = this.registry.get(name); + + if (!registration) { + throw new Error(`Service "${name}" not registered`); + } + + if (registration.type === "instance") { + return registration.instance; + } + + if (resolutionStack.has(name)) { + throw new Error( + `Circular dependency detected: ${[...resolutionStack, name].join( + " -> " + )}` + ); + } + + if (registration.singleton && registration.instance !== null) { + return registration.instance; + } + + resolutionStack.add(name); + const deps = (registration.dependencies || []).map((depName) => + this.resolve(depName, resolutionStack) + ); + resolutionStack.delete(name); + + let instance; + if (registration.type === "class") { + instance = new registration.value(...deps); + } else { + instance = registration.value(...deps); + } + + if (registration.singleton) { + registration.instance = instance; + } + + return instance; } /** @@ -87,7 +200,7 @@ class Container { */ has(name) { // TODO: Implement has - throw new Error("Not implemented"); + return this.registry.has(name); } /** @@ -97,7 +210,7 @@ class Container { */ unregister(name) { // TODO: Implement unregister - throw new Error("Not implemented"); + return this.registry.delete(name); } /** @@ -105,7 +218,7 @@ class Container { */ clear() { // TODO: Implement clear - throw new Error("Not implemented"); + this.registry.clear(); } /** @@ -114,7 +227,7 @@ class Container { */ getRegistrations() { // TODO: Implement getRegistrations - throw new Error("Not implemented"); + return Array.from(this.registry.keys()); } } @@ -130,9 +243,30 @@ function createChildContainer(parent) { // Create a new container that: // - First checks its own registry // - Falls back to parent for unregistered services + if (!(parent instanceof Container)) { + throw new Error("Parent must be a Container instance"); + } const child = new Container(); // Override resolve to check parent... + const originalResolve = child.resolve.bind(child); + child.resolve = function (name, resolutionStack = new Set()) { + if (child.has(name)) { + return originalResolve(name, resolutionStack); + } + return parent.resolve(name, resolutionStack); + }; + + child.has = function (name) { + return child.registry.has(name) || parent.has(name); + }; + + child.getRegistrations = function () { + return Array.from( + new Set([...child.registry.keys(), ...parent.getRegistrations()]) + ); + }; + return child; } From 9f10933de4e6c7ed20db8fc1d0f6de117152438f Mon Sep 17 00:00:00 2001 From: Bexultan Date: Mon, 15 Dec 2025 17:03:43 +0500 Subject: [PATCH 15/18] complete assignment 16: state machine --- 16-state-machine/index.js | 159 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 152 insertions(+), 7 deletions(-) diff --git a/16-state-machine/index.js b/16-state-machine/index.js index d0bad18..d347169 100644 --- a/16-state-machine/index.js +++ b/16-state-machine/index.js @@ -17,6 +17,25 @@ class StateMachine { // this.currentState = config.initial; // this.context = config.context || {}; // Step 3: Validate initial state exists in states + + if (!config || typeof config !== "object") { + throw new Error("Config must be an object"); + } + if (!config.initial || typeof config.initial !== "string") { + throw new Error('Config must have a valid "initial" state'); + } + if (!config.states || typeof config.states !== "object") { + throw new Error('Config must have a "states" object'); + } + if (!config.states[config.initial]) { + throw new Error( + `Initial state "${config.initial}" not defined in states` + ); + } + + this._config = config; + this._currentState = config.initial; + this._context = config.context ? { ...config.context } : {}; } /** @@ -25,7 +44,7 @@ class StateMachine { */ get state() { // TODO: Return current state - throw new Error("Not implemented"); + return this._currentState; } /** @@ -55,7 +74,56 @@ class StateMachine { // Step 7: Return true - throw new Error("Not implemented"); + const currentStateConfig = this._config.states[this._currentState]; + if (!currentStateConfig || !currentStateConfig.on) { + return false; + } + + const transitionConfig = currentStateConfig.on[event]; + if (!transitionConfig) { + return false; + } + + let target; + let guard; + let action; + + if (typeof transitionConfig === "string") { + target = transitionConfig; + } else if (typeof transitionConfig === "object") { + target = transitionConfig.target; + guard = transitionConfig.guard; + action = transitionConfig.action; + } else { + return false; + } + + if (!target || !this._config.states[target]) { + return false; + } + + if (guard && typeof guard === "function") { + try { + if (!guard(this._context, payload)) { + return false; + } + } catch (err) { + console.error("Guard threw an error:", err); + return false; + } + } + + this._currentState = target; + + if (action && typeof action === "function") { + try { + action(this._context, payload); + } catch (err) { + console.error("Action threw an error:", err); + } + } + + return true; } /** @@ -69,7 +137,42 @@ 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 || !currentStateConfig.on) { + return false; + } + + const transitionConfig = currentStateConfig.on[event]; + if (!transitionConfig) { + return false; + } + + let target; + let guard; + + if (typeof transitionConfig === "string") { + target = transitionConfig; + } else if (typeof transitionConfig === "object") { + target = transitionConfig.target; + guard = transitionConfig.guard; + } else { + return false; + } + + if (!target || !this._config.states[target]) { + return false; + } + + if (guard && typeof guard === "function") { + try { + return guard(this._context); + } catch (err) { + console.error("Guard threw an error:", err); + return false; + } + } + + return true; } /** @@ -81,7 +184,14 @@ class StateMachine { // Return array of event names from current state's 'on' config - throw new Error("Not implemented"); + const currentStateConfig = this._config.states[this._currentState]; + if (!currentStateConfig || !currentStateConfig.on) { + return []; + } + + return Object.keys(currentStateConfig.on).filter((event) => + this.can(event) + ); } /** @@ -90,7 +200,7 @@ class StateMachine { */ getContext() { // TODO: Return context - throw new Error("Not implemented"); + return { ...this._context }; } /** @@ -101,6 +211,13 @@ 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" && updater !== null) { + this._context = { ...this._context, ...updater }; + } else { + throw new Error("Updater must be an object or function"); + } } /** @@ -109,7 +226,12 @@ 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 || + !currentStateConfig.on || + Object.keys(currentStateConfig.on).length === 0 + ); } /** @@ -119,6 +241,13 @@ class StateMachine { reset(newContext) { // TODO: Reset to initial state // Optionally reset context + this._currentState = this._config.initial; + if (newContext !== undefined) { + this._context = + newContext && typeof newContext === "object" ? { ...newContext } : {}; + } else { + this._context = this._config.context ? { ...this._config.context } : {}; + } } } @@ -134,7 +263,23 @@ function createMachine(config) { // Return a function that creates new StateMachine instances // with the given config - return () => new StateMachine(config); + if (!config || typeof config !== "object") { + throw new Error("Config must be an object"); + } + if (!config.initial || typeof config.initial !== "string") { + throw new Error('Config must have a valid "initial" state'); + } + if (!config.states || typeof config.states !== "object") { + throw new Error('Config must have a "states" object'); + } + + return function (initialContext) { + const fullConfig = { + ...config, + context: initialContext || config.context, + }; + return new StateMachine(fullConfig); + }; } module.exports = { StateMachine, createMachine }; From 05d9f9038f03c370e3e6b7296f6dc096e7dd42d4 Mon Sep 17 00:00:00 2001 From: Bexultan Date: Mon, 15 Dec 2025 17:10:33 +0500 Subject: [PATCH 16/18] complete assignment 17: command pattern --- 17-command-pattern/index.js | 86 +++++++++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 9 deletions(-) diff --git a/17-command-pattern/index.js b/17-command-pattern/index.js index e8cd005..b2e23f9 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 = []; } /** @@ -23,6 +23,13 @@ class CommandManager { // Step 1: Call command.execute() // Step 2: Push to undo stack // Step 3: Clear redo stack (new action invalidates redo history) + if (!command || typeof command.execute !== "function") { + throw new Error("Command must have an execute() method"); + } + + command.execute(); + this.undoStack.push(command); + this.redoStack = []; } /** @@ -42,7 +49,18 @@ class CommandManager { // Step 5: Return true - throw new Error("Not implemented"); + if (this.undoStack.length === 0) { + return false; + } + + const command = this.undoStack.pop(); + if (typeof command.undo !== "function") { + throw new Error("Command must have an undo() method"); + } + + command.undo(); + this.redoStack.push(command); + return true; } /** @@ -62,7 +80,14 @@ class CommandManager { // Step 5: Return true - throw new Error("Not implemented"); + if (this.redoStack.length === 0) { + return false; + } + + const command = this.redoStack.pop(); + command.execute(); + this.undoStack.push(command); + return true; } /** @@ -71,7 +96,7 @@ class CommandManager { */ canUndo() { // TODO: Return whether undo stack has items - throw new Error("Not implemented"); + return this.undoStack.length > 0; } /** @@ -80,7 +105,7 @@ class CommandManager { */ canRedo() { // TODO: Return whether redo stack has items - throw new Error("Not implemented"); + return this.redoStack.length > 0; } /** @@ -89,7 +114,7 @@ class CommandManager { */ get history() { // TODO: Return copy of undo stack - throw new Error("Not implemented"); + return [...this.undoStack]; } /** @@ -97,6 +122,8 @@ class CommandManager { */ clear() { // TODO: Clear both stacks + this.undoStack = []; + this.redoStack = []; } } @@ -106,17 +133,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 +155,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 +177,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 +204,27 @@ 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("Division 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; + } } } @@ -185,6 +237,7 @@ class MacroCommand { constructor(commands = []) { // TODO: Store commands array // this.commands = commands; + this.commands = [...commands]; this.description = "Macro"; } @@ -194,14 +247,21 @@ class MacroCommand { */ add(command) { // TODO: Add command to array + this.commands.push(command); } execute() { // TODO: Execute all commands in order + for (const cmd of this.commands) { + cmd.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 +273,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; + } } } From cd4fdf5588f289ebe7b394f76fb99977cb495f99 Mon Sep 17 00:00:00 2001 From: Bexultan Date: Mon, 15 Dec 2025 17:23:24 +0500 Subject: [PATCH 17/18] complete assignment 18: strategy pattern --- 18-strategy-pattern/index.js | 134 ++++++++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 19 deletions(-) diff --git a/18-strategy-pattern/index.js b/18-strategy-pattern/index.js index 807bcd2..76f1baa 100644 --- a/18-strategy-pattern/index.js +++ b/18-strategy-pattern/index.js @@ -14,17 +14,18 @@ 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"); + return this.strategy.sort([...array]); } } @@ -36,7 +37,16 @@ class BubbleSort { // 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 +58,15 @@ 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 = arr.filter((x) => x < pivot); + const middle = arr.filter((x) => x === pivot); + const right = arr.filter((x) => x > pivot); + + return [...this.sort(left), ...middle, ...this.sort(right)]; } } @@ -60,7 +78,30 @@ 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; + let j = 0; + + while (i < left.length && j < right.length) { + if (left[i] <= right[j]) { + result.push(left[i++]); + } else { + result.push(right[j++]); + } + } + + return result.concat(left.slice(i)).concat(right.slice(j)); } } @@ -76,15 +117,17 @@ class MergeSort { class PricingContext { constructor(strategy) { // TODO: Store strategy + this.strategy = strategy; } setStrategy(strategy) { // TODO: Update strategy + this.strategy = strategy; } calculateTotal(items) { // TODO: Delegate to strategy - throw new Error("Not implemented"); + return this.strategy.calculate(items); } } @@ -94,7 +137,7 @@ class PricingContext { class RegularPricing { calculate(items) { // TODO: Sum all item prices - throw new Error("Not implemented"); + return items.reduce((sum, item) => sum + item.price, 0); } } @@ -104,13 +147,14 @@ class RegularPricing { class PercentageDiscount { constructor(percentage) { // TODO: Store percentage (0-100) - // this.percentage = percentage; + this.percentage = percentage; } calculate(items) { // TODO: Apply percentage discount // total * (1 - percentage/100) - throw new Error("Not implemented"); + const total = items.reduce((sum, item) => sum + item.price, 0); + return total * (1 - this.percentage / 100); } } @@ -120,13 +164,14 @@ class PercentageDiscount { class FixedDiscount { constructor(amount) { // TODO: Store fixed discount amount - // this.amount = amount; + this.amount = amount; } calculate(items) { // TODO: Subtract fixed amount from total // Don't go below 0 - throw new Error("Not implemented"); + const total = items.reduce((sum, item) => sum + item.price, 0); + return Math.max(0, total - this.amount); } } @@ -137,7 +182,15 @@ 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 sortedPrices = items.map((item) => item.price).sort((a, b) => b - a); + + let total = 0; + for (let i = 0; i < sortedPrices.length; i++) { + if (i % 2 === 0) { + total += sortedPrices[i]; + } + } + return total; } } @@ -151,11 +204,23 @@ 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((sum, item) => sum + item.price, 0); + let applicableDiscount = 0; + + for (const tier of this.tiers) { + if (subtotal >= tier.threshold) { + applicableDiscount = tier.discount; + } else { + break; + } + } + + return subtotal * (1 - applicableDiscount / 100); } } @@ -169,15 +234,17 @@ class TieredDiscount { class ValidationContext { constructor(strategy) { // TODO: Store strategy + this.strategy = strategy; } setStrategy(strategy) { // TODO: Update strategy + this.strategy = strategy; } validate(data) { // TODO: Delegate to strategy - throw new Error("Not implemented"); + return this.strategy.validate(data); } } @@ -194,7 +261,35 @@ class StrictValidation { // TODO: Validate that name, email, and age are all present and valid // Return { valid: boolean, errors: string[] } // Example: { valid: false, errors: ["Name is required", "Email is required"] } - throw new Error("Not implemented"); + const errors = []; + + if ( + !Object.prototype.hasOwnProperty.call(data, "name") || + typeof data.name !== "string" || + data.name.trim() === "" + ) { + errors.push("Name must be a non-empty string"); + } + + if ( + !Object.prototype.hasOwnProperty.call(data, "email") || + typeof data.email !== "string" || + data.email.trim() === "" + ) { + errors.push("Email must be a non-empty string"); + } + + if ( + !Object.prototype.hasOwnProperty.call(data, "age") || + typeof data.age !== "number" + ) { + errors.push("Age must be a number"); + } + + return { + valid: errors.length === 0, + errors, + }; } } @@ -208,7 +303,7 @@ class LenientValidation { validate(data) { // TODO: Always return valid: true, errors: [] // This strategy has no validation rules - return { valid: false, errors: ["Not implemented"] }; // Broken: Replace with implementation + return { valid: true, errors: [] }; } } @@ -224,21 +319,22 @@ class LenientValidation { class StrategyRegistry { constructor() { // TODO: Initialize registry map - // this.strategies = new Map(); + this.strategies = new Map(); } register(name, strategy) { // TODO: Store strategy by name + this.strategies.set(name, strategy); } get(name) { // TODO: Return strategy by name - throw new Error("Not implemented"); + return this.strategies.get(name) || null; } has(name) { // TODO: Check if strategy exists - throw new Error("Not implemented"); + return this.strategies.has(name); } } From de0d4db85d8f8906a16fc46a899a7274885e8b59 Mon Sep 17 00:00:00 2001 From: Bexultan Date: Mon, 15 Dec 2025 19:23:29 +0500 Subject: [PATCH 18/18] completed all the assignments --- 19-proxy-pattern/index.js | 103 ++++++++++++++---- 20-builder-pattern/index.js | 207 ++++++++++++++++++++++++++++-------- 2 files changed, 243 insertions(+), 67 deletions(-) diff --git a/19-proxy-pattern/index.js b/19-proxy-pattern/index.js index 6d2bf2b..a636354 100644 --- a/19-proxy-pattern/index.js +++ b/19-proxy-pattern/index.js @@ -26,13 +26,21 @@ function createValidatingProxy(target, validators) { // Set property if passes // Broken: doesn't set at all (fails all tests) + const validator = validators[prop]; + if (validator) { + if (!validator(value)) { + throw new Error(`Validation failed for property "${prop}"`); + } + } + // No validator means no restriction – allow the set + obj[prop] = value; return true; }, - get(obj, prop) { + get(obj, prop, receiver) { // TODO: Implement get trap // Broken: returns wrong value - return "NOT_IMPLEMENTED"; + return Reflect.get(obj, prop, receiver); }, }); } @@ -48,24 +56,31 @@ function createLoggingProxy(target, logger) { // TODO: Implement logging proxy return new Proxy(target, { - get(obj, prop) { + get(obj, prop, receiver) { // TODO: Log 'get' and return value - throw new Error("Not implemented"); + const value = Reflect.get(obj, prop, receiver); + logger("get", prop, value); + return value; }, - set(obj, prop, value) { + set(obj, prop, value, receiver) { // TODO: Log 'set' and set value - throw new Error("Not implemented"); + logger("set", prop, value); + return Reflect.set(obj, prop, value, receiver); }, deleteProperty(obj, prop) { // TODO: Log 'delete' and delete property - throw new Error("Not implemented"); + const value = obj[prop]; + logger("delete", prop, value); + return Reflect.deleteProperty(obj, prop); }, 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,10 +96,10 @@ 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) { + get(obj, prop, receiver) { // TODO: Implement get trap // If prop is in methodNames and is a function: @@ -95,7 +110,24 @@ function createCachingProxy(target, methodNames) { // Otherwise, return property normally - throw new Error("Not implemented"); + const value = Reflect.get(obj, prop, receiver); + + // If the property is one of the methods we want to cache and it's a function + if (methodNames.includes(prop) && typeof value === "function") { + return function (...args) { + const cacheKey = `${prop}|${JSON.stringify(args)}`; + + if (cache.has(cacheKey)) { + return cache.get(cacheKey); + } + + const result = value.apply(this === receiver ? obj : this, args); + cache.set(cacheKey, result); + return result; + }; + } + + return value; }, }); } @@ -115,24 +147,33 @@ function createAccessProxy(target, permissions) { const { readable = [], writable = [] } = permissions; return new Proxy(target, { - get(obj, prop) { + get(obj, prop, receiver) { // 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(`Access denied: cannot read property "${prop}"`); + } + return Reflect.get(obj, prop, receiver); }, - set(obj, prop, value) { + set(obj, prop, value, receiver) { // TODO: Check if prop is in writable // Throw if not allowed // Broken: doesn't actually set - return true; + if (!writable.includes(prop)) { + throw new Error(`Access denied: property "${prop}" is read-only`); + } + return Reflect.set(obj, prop, value, receiver); }, deleteProperty(obj, prop) { // TODO: Only allow if in writable // Broken: doesn't delete - return true; + if (!writable.includes(prop)) { + throw new Error(`Access denied: cannot delete property "${prop}"`); + } + return Reflect.deleteProperty(obj, prop); }, }); } @@ -152,18 +193,22 @@ function createLazyProxy(loader) { return new Proxy( {}, { - get(obj, prop) { + get(_, prop, receiver) { // 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 Reflect.get(instance, prop, receiver); }, - set(obj, prop, value) { + set(_, prop, value, receiver) { // TODO: Load instance if needed, then set throw new Error("Not implemented"); }, - }, + } ); } @@ -178,14 +223,26 @@ function createObservableProxy(target, onChange) { // TODO: Implement observable proxy return new Proxy(target, { - set(obj, prop, value) { + set(obj, prop, value, receiver) { // TODO: Call onChange(prop, value, oldValue) on change - throw new Error("Not implemented"); + const oldValue = obj[prop]; + if (value !== oldValue) { + const result = Reflect.set(obj, prop, value, receiver); + onChange(prop, value, oldValue); + return result; + } + return true; }, deleteProperty(obj, prop) { // TODO: Call onChange on delete - throw new Error("Not implemented"); + if (prop in obj) { + const oldValue = obj[prop]; + const result = Reflect.deleteProperty(obj, prop); + onChange(prop, undefined, oldValue); + return result; + } + return true; }, }); } diff --git a/20-builder-pattern/index.js b/20-builder-pattern/index.js index 69a1e1a..424d97b 100644 --- a/20-builder-pattern/index.js +++ b/20-builder-pattern/index.js @@ -10,11 +10,11 @@ class QueryBuilder { constructor() { // TODO: Initialize state - // this.selectCols = []; - // this.fromTable = null; - // this.whereClauses = []; - // this.orderByClauses = []; - // this.limitCount = null; + this.selectCols = []; + this.fromTable = null; + this.whereClauses = []; + this.orderByClauses = []; + this.limitCount = null; } /** @@ -24,7 +24,8 @@ class QueryBuilder { */ select(...columns) { // TODO: Store columns - throw new Error("Not implemented"); + this.selectCols.push(...columns); + return this; } /** @@ -34,7 +35,8 @@ class QueryBuilder { */ from(table) { // TODO: Store table name - throw new Error("Not implemented"); + this.fromTable = table; + return this; } /** @@ -46,7 +48,18 @@ class QueryBuilder { */ where(column, operator, value) { // TODO: Store where clause - throw new Error("Not implemented"); + let valStr; + if (typeof value === "string") { + valStr = `'${value.replace(/'/g, "\\'")}'`; + } else if (typeof value === "boolean") { + valStr = value ? "true" : "false"; + } else if (value === null) { + valStr = "NULL"; + } else { + valStr = value.toString(); + } + this.whereClauses.push(`${column} ${operator} ${valStr}`); + return this; } /** @@ -57,7 +70,8 @@ class QueryBuilder { */ orderBy(column, direction = "ASC") { // TODO: Store order by clause - throw new Error("Not implemented"); + this.orderByClauses.push(`${column} ${direction.toUpperCase()}`); + return this; } /** @@ -67,7 +81,8 @@ class QueryBuilder { */ limit(count) { // TODO: Store limit - throw new Error("Not implemented"); + this.limitCount = count; + return this; } /** @@ -77,7 +92,26 @@ 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("From table is required"); + } + let query = "SELECT "; + if (this.selectCols.length === 0) { + query += "*"; + } else { + query += this.selectCols.join(", "); + } + query += ` FROM ${this.fromTable}`; + if (this.whereClauses.length > 0) { + query += ` WHERE ${this.whereClauses.join(" AND ")}`; + } + if (this.orderByClauses.length > 0) { + query += ` ORDER BY ${this.orderByClauses.join(", ")}`; + } + if (this.limitCount !== null) { + query += ` LIMIT ${this.limitCount}`; + } + return query; } /** @@ -86,7 +120,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 +137,12 @@ class QueryBuilder { class HTMLBuilder { constructor() { // TODO: Initialize state - // this.tagName = 'div'; - // this.idAttr = null; - // this.classes = []; - // this.attributes = {}; - // this.innerContent = ''; - // this.children = []; + this.tagName = "div"; + this.idAttr = null; + this.classes = []; + this.attributes = {}; + this.innerContent = ""; + this.children = []; } /** @@ -113,7 +152,8 @@ class HTMLBuilder { */ tag(name) { // TODO: Store tag name - throw new Error("Not implemented"); + this.tagName = name; + return this; } /** @@ -123,7 +163,8 @@ class HTMLBuilder { */ id(id) { // TODO: Store id - throw new Error("Not implemented"); + this.idAttr = id; + return this; } /** @@ -133,7 +174,8 @@ class HTMLBuilder { */ class(...classNames) { // TODO: Store classes - throw new Error("Not implemented"); + this.classes.push(...classNames); + return this; } /** @@ -144,7 +186,8 @@ class HTMLBuilder { */ attr(name, value) { // TODO: Store attribute - throw new Error("Not implemented"); + this.attributes[name] = value; + return this; } /** @@ -154,7 +197,8 @@ class HTMLBuilder { */ content(content) { // TODO: Store content - throw new Error("Not implemented"); + this.innerContent = content; + return this; } /** @@ -164,7 +208,8 @@ class HTMLBuilder { */ child(childHtml) { // TODO: Store child - throw new Error("Not implemented"); + this.children.push(childHtml); + return this; } /** @@ -174,7 +219,22 @@ class HTMLBuilder { build() { // TODO: Build and return HTML string // Format: content - throw new Error("Not implemented"); + if (!this.tagName) { + throw new Error("Tag name is required"); + } + let attrs = ""; + if (this.idAttr) { + attrs += ` id="${this.idAttr}"`; + } + if (this.classes.length > 0) { + attrs += ` class="${this.classes.join(" ")}"`; + } + for (const [name, value] of Object.entries(this.attributes)) { + attrs += ` ${name}="${value}"`; + } + let inner = this.innerContent || ""; + inner += this.children.join(""); + return `<${this.tagName}${attrs}>${inner}`; } /** @@ -183,7 +243,13 @@ class HTMLBuilder { */ reset() { // TODO: Reset all state - throw new Error("Not implemented"); + this.tagName = null; + this.idAttr = null; + this.classes = []; + this.attributes = {}; + this.innerContent = null; + this.children = []; + return this; } } @@ -195,12 +261,12 @@ class HTMLBuilder { class ConfigBuilder { constructor() { // TODO: Initialize state - // this.config = { - // environment: 'development', - // database: null, - // features: [], - // logLevel: 'info' - // }; + this.config = { + environment: "development", + database: null, + features: new Set(), + logLevel: "info", + }; } /** @@ -210,7 +276,8 @@ class ConfigBuilder { */ setEnvironment(env) { // TODO: Set environment - throw new Error("Not implemented"); + this.config.environment = env; + return this; } /** @@ -220,7 +287,8 @@ class ConfigBuilder { */ setDatabase(dbConfig) { // TODO: Set database config - throw new Error("Not implemented"); + this.config.database = dbConfig; + return this; } /** @@ -230,7 +298,8 @@ class ConfigBuilder { */ enableFeature(feature) { // TODO: Add feature to list - throw new Error("Not implemented"); + this.config.features.add(feature); + return this; } /** @@ -240,7 +309,8 @@ class ConfigBuilder { */ disableFeature(feature) { // TODO: Remove feature from list - throw new Error("Not implemented"); + this.config.features.delete(feature); + return this; } /** @@ -250,7 +320,8 @@ class ConfigBuilder { */ setLogLevel(level) { // TODO: Set log level - throw new Error("Not implemented"); + this.config.logLevel = level; + return this; } /** @@ -259,7 +330,10 @@ class ConfigBuilder { */ build() { // TODO: Return copy of config - throw new Error("Not implemented"); + return { + ...this.config, + features: [...this.config.features], + }; } } @@ -271,6 +345,12 @@ class ConfigBuilder { class RequestBuilder { constructor(baseUrl = "") { // TODO: Initialize state + this.baseUrl = baseUrl; + this.httpMethod = null; + this.urlPath = null; + this.queryParams = new Map(); + this.headersMap = new Map(); + this.requestBody = null; } /** @@ -279,7 +359,8 @@ class RequestBuilder { * @returns {RequestBuilder} this */ method(method) { - throw new Error("Not implemented"); + this.httpMethod = method.toUpperCase(); + return this; } /** @@ -288,7 +369,8 @@ class RequestBuilder { * @returns {RequestBuilder} this */ path(path) { - throw new Error("Not implemented"); + this.urlPath = path; + return this; } /** @@ -298,7 +380,8 @@ class RequestBuilder { * @returns {RequestBuilder} this */ query(key, value) { - throw new Error("Not implemented"); + this.queryParams.set(key, value); + return this; } /** @@ -308,7 +391,8 @@ class RequestBuilder { * @returns {RequestBuilder} this */ header(key, value) { - throw new Error("Not implemented"); + this.headersMap.set(key, value); + return this; } /** @@ -317,7 +401,8 @@ class RequestBuilder { * @returns {RequestBuilder} this */ body(body) { - throw new Error("Not implemented"); + this.requestBody = body; + return this; } /** @@ -326,7 +411,41 @@ class RequestBuilder { */ build() { // TODO: Return fetch-compatible config - throw new Error("Not implemented"); + let url = this.baseUrl; + if (this.urlPath) { + url = url.endsWith("/") ? url : url + "/"; + url += this.urlPath.replace(/^\/+/, ""); + } + if (this.queryParams.size > 0) { + const params = Array.from(this.queryParams.entries()) + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) + .join("&"); + url += (url.includes("?") ? "&" : "?") + params; + } + + const config = { + method: this.httpMethod || "GET", + url, + headers: Object.fromEntries(this.headersMap), + }; + + if (this.requestBody !== null) { + if ( + typeof this.requestBody === "object" && + this.requestBody !== null && + !(this.requestBody instanceof FormData) + ) { + config.body = this.requestBody; + + if (!config.headers["Content-Type"]) { + config.headers["Content-Type"] = "application/json"; + } + } else { + config.body = this.requestBody; + } + } + + return config; } }