לדלג לתוכן

4.5 פרומיסים ו async await פתרון

פתרון - פרומיסים ו-async/await


תרגיל 1 - פרומיסים בסיסיים

חלק א:

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// test
async function demo() {
    console.log("Start");
    await delay(2000);
    console.log("2 seconds later");
}
demo();

חלק ב:

function randomSuccess(chance) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() < chance) {
                resolve("Operation succeeded");
            } else {
                reject(new Error("Operation failed"));
            }
        }, 1000);
    });
}

// test
randomSuccess(0.7)
    .then((msg) => console.log(msg))
    .catch((err) => console.log(err.message));

תרגיל 2 - שרשור Promises

function fetchUser(id) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve({ id, name: "Alice", departmentId: 10 });
        }, 500);
    });
}

function fetchDepartment(id) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve({ id, name: "Engineering", managerId: 42 });
        }, 500);
    });
}

function fetchManager(id) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve({ id, name: "Bob", email: "bob@company.com" });
        }, 500);
    });
}

גרסה עם שרשור then:

function getUserManagerEmail(userId) {
    return fetchUser(userId)
        .then(user => fetchDepartment(user.departmentId))
        .then(dept => fetchManager(dept.managerId))
        .then(manager => manager.email);
}

getUserManagerEmail(1).then(email => console.log(email));
// "bob@company.com" (after ~1.5 seconds)

גרסה עם async/await:

async function getUserManagerEmail(userId) {
    const user = await fetchUser(userId);
    const department = await fetchDepartment(user.departmentId);
    const manager = await fetchManager(department.managerId);
    return manager.email;
}

const email = await getUserManagerEmail(1);
console.log(email); // "bob@company.com"

תרגיל 3 - Promise.all לעומת ריצה סדרתית

function fetchUsers() {
    return new Promise(resolve =>
        setTimeout(() => resolve(["Alice", "Bob"]), 1000)
    );
}

function fetchPosts() {
    return new Promise(resolve =>
        setTimeout(() => resolve(["Post 1", "Post 2"]), 1000)
    );
}

function fetchComments() {
    return new Promise(resolve =>
        setTimeout(() => resolve(["Comment 1"]), 1000)
    );
}

חלק א - סדרתי:

async function loadAllSequential() {
    const users = await fetchUsers();
    const posts = await fetchPosts();
    const comments = await fetchComments();
    return { users, posts, comments };
}

console.time("sequential");
const data = await loadAllSequential();
console.timeEnd("sequential"); // ~3000ms
console.log(data);

חלק ב - מקבילי:

async function loadAllParallel() {
    const [users, posts, comments] = await Promise.all([
        fetchUsers(),
        fetchPosts(),
        fetchComments()
    ]);
    return { users, posts, comments };
}

console.time("parallel");
const data2 = await loadAllParallel();
console.timeEnd("parallel"); // ~1000ms
console.log(data2);

ההבדל: סדרתי לוקח ~3 שניות (1+1+1), מקבילי לוקח ~1 שניה (הכל רץ ביחד).


תרגיל 4 - טיפול בשגיאות

async function safeFetch(url) {
    try {
        const response = await fetch(url);

        if (!response.ok) {
            throw new Error(`HTTP error: ${response.status}`);
        }

        const data = await response.json();
        return { ok: true, data };
    } catch (error) {
        return { ok: false, error: error.message };
    } finally {
        console.log("Done");
    }
}

// test
const result = await safeFetch("/api/users");
if (result.ok) {
    console.log("Data:", result.data);
} else {
    console.log("Error:", result.error);
}
async function fetchMultiple(urls) {
    const results = await Promise.allSettled(
        urls.map(url => safeFetch(url))
    );

    return results.map((result, i) => ({
        url: urls[i],
        ...result.value // safeFetch always returns { ok, data/error }
    }));
}

// test
const results = await fetchMultiple([
    "/api/users",
    "/api/nonexistent",
    "/api/posts"
]);

results.forEach(r => {
    if (r.ok) {
        console.log(`${r.url}: success`);
    } else {
        console.log(`${r.url}: failed - ${r.error}`);
    }
});

תרגיל 5 - retry עם backoff

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function retryAsync(fn, maxRetries = 3, initialDelay = 1000) {
    let lastError;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const result = await fn();
            console.log(`Attempt ${attempt} succeeded!`);
            return result;
        } catch (error) {
            lastError = error;
            const delayMs = initialDelay * Math.pow(2, attempt - 1);

            if (attempt < maxRetries) {
                console.log(
                    `Attempt ${attempt} failed: ${error.message} (retrying in ${delayMs}ms)`
                );
                await delay(delayMs);
            } else {
                console.log(`Attempt ${attempt} failed: ${error.message} (no more retries)`);
            }
        }
    }

    throw lastError;
}

// test
let callCount = 0;

async function unreliableApi() {
    callCount++;
    if (callCount < 3) {
        throw new Error("Server error");
    }
    return { data: "success" };
}

const result = await retryAsync(unreliableApi);
console.log(result); // { data: "success" }

// Output:
// Attempt 1 failed: Server error (retrying in 1000ms)
// Attempt 2 failed: Server error (retrying in 2000ms)
// Attempt 3 succeeded!
// { data: "success" }

תרגיל 6 - Promise.race - timeout

חלק א:

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function withTimeout(promise, ms) {
    const timeout = new Promise((_, reject) => {
        setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
    });

    return Promise.race([promise, timeout]);
}

// test
const fast = delay(500).then(() => "fast result");
const slow = delay(5000).then(() => "slow result");

console.log(await withTimeout(fast, 1000)); // "fast result"

try {
    await withTimeout(slow, 1000);
} catch (error) {
    console.log(error.message); // "Timeout after 1000ms"
}

חלק ב:

async function fetchFastest(urls) {
    const fetchPromises = urls.map(async (url) => {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error(`HTTP ${response.status} from ${url}`);
        }
        return response.json();
    });

    return Promise.any(fetchPromises);
}

// test
try {
    const result = await fetchFastest([
        "https://slow-api.com/data",
        "https://fast-api.com/data",
        "https://medium-api.com/data"
    ]);
    console.log("Fastest result:", result);
} catch (error) {
    // AggregateError - all failed
    console.log("All requests failed:", error.errors);
}

Promise.any מחזיר את התוצאה של ה-Promise הראשון שמצליח. אם כולם נכשלים, זורק AggregateError עם כל השגיאות.


תרגיל 7 - עיבוד תור משימות

class AsyncQueue {
    #queue = [];
    #processing = false;

    async enqueue(asyncFn) {
        return new Promise((resolve, reject) => {
            this.#queue.push({ asyncFn, resolve, reject });
            this.#processNext();
        });
    }

    async #processNext() {
        if (this.#processing || this.#queue.length === 0) {
            return;
        }

        this.#processing = true;
        const { asyncFn, resolve, reject } = this.#queue.shift();

        try {
            const result = await asyncFn();
            resolve(result);
        } catch (error) {
            reject(error);
        } finally {
            this.#processing = false;
            this.#processNext();
        }
    }

    getQueueSize() {
        return this.#queue.length;
    }

    isProcessing() {
        return this.#processing;
    }
}

// helper
function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// test
const queue = new AsyncQueue();

queue.enqueue(async () => {
    await delay(1000);
    console.log("Task 1 done");
    return "result 1";
});

queue.enqueue(async () => {
    await delay(500);
    console.log("Task 2 done");
    return "result 2";
});

const result3 = await queue.enqueue(async () => {
    await delay(200);
    console.log("Task 3 done");
    return "result 3";
});

console.log(result3); // "result 3"
// Output:
// "Task 1 done" (after 1s)
// "Task 2 done" (after 1.5s)
// "Task 3 done" (after 1.7s)
// "result 3"

איך זה עובד:
1. enqueue יוצרת Promise חדש ושומרת את resolve/reject שלו בתור
2. #processNext בודקת אם יש משימה בתור ואם לא רצה כבר משימה
3. כשמשימה מסתיימת, finally קורא ל-#processNext שוב כדי לעבד את הבאה
4. ה-Promise שהוחזר ל-caller מתקיים כשהמשימה שלו (לא הראשונה בתור) מסתיימת

בונוס - concurrency:

class AsyncQueue {
    #queue = [];
    #running = 0;
    #concurrency;

    constructor(concurrency = 1) {
        this.#concurrency = concurrency;
    }

    async enqueue(asyncFn) {
        return new Promise((resolve, reject) => {
            this.#queue.push({ asyncFn, resolve, reject });
            this.#processNext();
        });
    }

    async #processNext() {
        if (this.#running >= this.#concurrency || this.#queue.length === 0) {
            return;
        }

        this.#running++;
        const { asyncFn, resolve, reject } = this.#queue.shift();

        try {
            const result = await asyncFn();
            resolve(result);
        } catch (error) {
            reject(error);
        } finally {
            this.#running--;
            this.#processNext();
        }
    }

    getQueueSize() {
        return this.#queue.length;
    }

    isProcessing() {
        return this.#running > 0;
    }
}

// with concurrency of 2:
const queue = new AsyncQueue(2);
// tasks 1 and 2 run in parallel, task 3 waits for one to finish

עם concurrency, במקום לבדוק this.#processing, סופרים כמה משימות רצות (#running) ומשווים ל-#concurrency. זה מאפשר לרוץ כמה משימות במקביל, אבל לא יותר מהמגבלה.