הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

חדש ב node - ניקוי משאבים אוטומטי עם using

11/05/2025

טייפסקריפט 5.2 הגיעה פקודה חדשה בשם using שתפקידה לאפשר לנו לנקות אוטומטית משאבים, קצת כמו with של פייתון. המנגנון בנוי בשתי שכבות:

  1. מקום אחד בקוד אחראי על יצירת המשאבים. אותו מקום שמקצה את המשאבים גם מגדיר את פונקציית הניקוי שלהם.

  2. מקום אחר בקוד "מפעיל" את המקום שיוצר משאבים, מקבל משאב ובאופן אוטומטי טייפסקריפט מזהה מתי המשאב שיצרנו יוצא מ scope ומנקה אותו.

טכנית זה עובד עם Symbol מיוחד בשם [Symbol.dispose]. הקוד שיוצר משאבים מחזיר אוביקט שאחד המפתחות בו הוא אותו Symbol.dispose, הערך של המפתח הזה הוא פונקציית הניקוי וכשצריך למחוק את המשאב טייפסקריפט יפעיל את הפונקציה.

דוגמה פשוטה לקוד שפותח קובץ נראית כך:

import { open } from "node:fs/promises";

const getFileHandle = async (path: string) => {
  const fh = await open(path, "w");

  return {
    fh,
    [Symbol.asyncDispose]: async () => {
      await fh.close();
    },
  };
};

הפונקציה פותחת קובץ ומחזירה גם את הקובץ וגם את פונקציית הסגירה באוביקט אחד, כשפונקציית הסגירה נשמרת במפתח Symbol.dispose או אם היא אסינכרונית כמו בדוגמה זה יהיה Symbol.asyncDispose.

במקום אחר בקוד אנחנו יוצרים את המשאב עם המילה החדשה using:

{
  await using file = await getFileHandle("thefile.txt");
  const { fh } = file;
  fh.write('hello world');
}

וכך ביציאה מהבלוק טייפסקריפט אוטומטי יודע להפעיל את פונקציית הניקוי. קוד הטייפסקריפט בדוגמה יהפוך לקוד ה JavaScript הבא:

var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
    if (value !== null && value !== void 0) {
        if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
        var dispose, inner;
        if (async) {
            if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
            dispose = value[Symbol.asyncDispose];
        }
        if (dispose === void 0) {
            if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
            dispose = value[Symbol.dispose];
            if (async) inner = dispose;
        }
        if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
        if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
        env.stack.push({ value: value, dispose: dispose, async: async });
    }
    else if (async) {
        env.stack.push({ async: true });
    }
    return value;
};
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
    return function (env) {
        function fail(e) {
            env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
            env.hasError = true;
        }
        var r, s = 0;
        function next() {
            while (r = env.stack.pop()) {
                try {
                    if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
                    if (r.dispose) {
                        var result = r.dispose.call(r.value);
                        if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
                    }
                    else s |= 1;
                }
                catch (e) {
                    fail(e);
                }
            }
            if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
            if (env.hasError) throw env.error;
        }
        return next();
    };
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
    var e = new Error(message);
    return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
});
import { open } from "node:fs/promises";
const getFileHandle = async (path) => {
    const fh = await open(path, "w");
    return {
        fh,
        [Symbol.asyncDispose]: async () => {
            await fh.close();
        },
    };
};
{
    const env_1 = { stack: [], error: void 0, hasError: false };
    try {
        const file = __addDisposableResource(env_1, await getFileHandle("thefile.txt"), true);
        const { fh } = file;
        fh.write('hello world');
    }
    catch (e_1) {
        env_1.error = e_1;
        env_1.hasError = true;
    }
    finally {
        const result_1 = __disposeResources(env_1);
        if (result_1)
            await result_1;
    }
} // Automatically disposed!

ולמה אני מספר לכם את כל זה עכשיו? הסיבה פשוטה ובכותרת. הפיצ'ר הגיעה השבוע ל node.js ותוכלו להשתמש בו גם בלי טייפסקריפט עם node 24. אני חושב שיש פה מדיניות של node.js לתמוך בטייפסקריפט בלי לקמפל לטייפסקריפט - מדיניות שהתחילה עם מנגנון ה strip types שלהם וממשיכה עם טיפול בפקודות הטייפסקריפט שלא קשורות רק לטיפוסים. עכשיו נשאר לחכות ל enum.

היכרות עם Drizzle

04/03/2025

דריזל היא ספריית העבודה עם בסיסי נתונים האהובה עליי בתקופה האחרונה. היא מספקת ביצועים מצוינים, כוללת המון פינוקים למפתחים וכמובן Type Safety ותיעוד מעולה. נכון, מספר הגירסה 0.40 לא משרה תחושת ביטחון אבל בואו נזכור שהספריה בפיתוח כבר שלוש שנים והיא אחת הפופולריות בתעשייה אז אולי אפשר לבלוע את הצפרדע הזאת.

בפוסט זה אספר לכם מספיק על דריזל כדי שתוכלו לכתוב פרויקט ראשון באמצעותה ואני מקווה שגם אתן לכם את החשק לעשות זאת.

המשך קריאה

מה עושים עם התלויות של התלויות ב node

20/07/2022

הנה טריק פשוט שעזר לי להתקין תלויות היום בפרויקט ואולי יעזור גם לכם. דמיינו רגע שיש לכם פרויקט שתלוי בספריה מסוימת בגירסה יחסית עדכנית שלה, ועכשיו אתם צריכים להוסיף ספריה אחרת לא כל כך מתוחזקת, או שמאיזושהי סיבה אתם צריכים להשתמש בגירסה ישנה של הספריה. לדוגמה הקובץ package.json הבא מכיל את גירסה 18 של ריאקט וגירסה יחסית ישנה של react-select:

{
  "name": "webapp-demo",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-select": "4.3.1"
  },
  "devDependencies": {
    "@types/react": "^18.0.15",
    "@types/react-dom": "^18.0.6",
    "@vitejs/plugin-react": "^2.0.0",
    "vite": "^3.0.0"
  }
}

אם תנסו לשים אותו בתיקיה ולהריץ npm install זה ייכשל, כי react-select גירסה 4.3.1 צריך את ריאקט 16 או 17, והפרויקט שלי משתמש בגירסה 18 של ריאקט. זאת הודעת השגיאה:

npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: webapp-demo@0.0.0
npm ERR! Found: react@18.2.0
npm ERR! node_modules/react
npm ERR!   react@"^18.2.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^16.8.0 || ^17.0.0" from react-select@4.3.1
npm ERR! node_modules/react-select
npm ERR!   react-select@"4.3.1" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR! See /Users/ynonp/.npm/eresolve-report.txt for a full report.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/ynonp/.npm/_logs/2022-07-19T12_28_29_781Z-debug-0.log

עכשיו אצלי על המכונה אני יודע להריץ את npm עם --force כשצריך, אבל משתמשים אולי ייבהלו מהודעה כזאת או מדרישה כזאת, וגם אנחנו לא רוצים לשנות את התנהגות בדיקת התלויות הכללית של npm, רק לגבי הספריה react-select אנחנו רוצים להתקין אותה למרות שגירסת ריאקט שלנו יותר חדשה.

מה שמביא אותנו לטריק של היום - המילה overrides. עם מפתח overrides אני יכול לשנות את הגירסאות של התלויות של התלויות שלי. בדוגמה שלנו נוסיף את המפתח ל package.json:

{
  "name": "webapp-demo",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-select": "4.3.1"
  },
  "devDependencies": {
    "@types/react": "^18.0.15",
    "@types/react-dom": "^18.0.6",
    "@vitejs/plugin-react": "^2.0.0",
    "vite": "^3.0.0"
  },
  "overrides": {
    "react-select": {
      "react": "18.2.0",
      "react-dom": "18.2.0"
    }
  }
}

והכל מותקן בשלום.

בבדיקה מה הותקן אני יכול לראות:

$ npm ls react

webapp-demo@0.0.0 /Users/ynonp/tmp/blog/webapp-demo
├─┬ react-dom@18.2.0
│ └── react@18.2.0 deduped
├─┬ react-select@4.3.1
│ ├─┬ @emotion/react@11.9.3
│ │ └── react@18.2.0 deduped
│ ├─┬ react-input-autosize@3.0.0
│ │ └── react@18.2.0 deduped invalid: "^16.3.0 || ^17.0.0" from node_modules/react-input-autosize
│ ├─┬ react-transition-group@4.4.2
│ │ └── react@18.2.0 deduped invalid: "^16.3.0 || ^17.0.0" from node_modules/react-input-autosize
│ └── react@18.2.0 deduped
└── react@18.2.0

כלומר התקנו את ריאקט 18.2 בלי שגיאות ובלי לקטר, למרות ש react-select ביקש גירסה נמוכה יותר. (וכמובן אף אחד לא מבטיח שזה יעבוד, אבל ספציפית בריאקט יש סיכוי טוב שכן כי בדרך כלל יש תאימות טובה אחורה).

חדש ב Node 18 - תמיכה מובנית ב Fetch API

25/04/2022

גירסה 18 של נוד מביאה איתה לא מעט חידושים: התמיכה ב ES Modules השתפרה פלאים, הוסיפו מודול מובנה להרצת בדיקות יחידה, ונוספה תמיכה לשני ממשקי רשת מהדפדפן שהם Fetch API ו Web Streams API.

בפוסט זה אציג את השימוש ב Fetch API ב Node.

המשך קריאה

שימו לב: נוד קורא גם את ה gitignore שלכם כשמפרסמים חבילות

17/03/2022

ישבתי היום אחר הצהריים לפרסם חבילה לפרויקט. בדרך כלל אני לא עובד אחרי הצהריים כי הילדים פה, אבל היו כמה דברים שרציתי לסיים וחשבתי שאני יכול לנסות קצת מולטי טאסקינג.

מפה לשם אני מנסה לבנות את החבילה ולפבלש, ובחלון אחר מפעיל npm install כדי לראות שזה עובד.

החבילה עצמה היא פרויקט צד לקוח שבניה שלו מייצרת תיקיה בשם dist, ובחלון השני שמשתמש בחבילה יש קוד שעושה import מאותה dist.

בקיצור בחזרה לבניה מפעיל publish בחלון אחד ו install בחלון השני ו... אין dist בספריה שהותקנה. נו, בטח שכחתי להפעיל build בין כל הילדים שקופצים פה מסביב.

ניסיון שני בבניה, שוב publish, שוב install ו... dist עדיין לא שם.

בסופו של דבר ברחתי מהילדים למקום שקט, הסתכלתי בשקט בספריה ומצאתי שם קובץ .gitignore עם השורה:

dist/

גיט לוג חשף שהיתה כוונה טובה מאחורי השורה הזאת - אין צורך להוסיף את כל תוצרי הבניה ל git. אבל היא באה עם באג: בלי קובץ .npmignore בתיקיה, באופן אוטומטי npm יתעלם מהקבצים ב .gitignore ולא יפרסם אותם עם החבילה שלכם.

וכמו תמיד אחרי שמבינים את הבעיה הפיתרון היה פשוט ליצור קובץ .npmignore ריק, ולהפסיק להאשים את הילדים בבאגים שלי...

בואו נכתוב שרת GraphQL ב Node.JS כדי לראות איך זה עובד

30/08/2021

גרף קיו אל הוא הדבר הגדול הבא מאז שהמציאו את הלחם החתוך. הוא פותר את כל הבעיות של REST ואז עוד קצת וכבר כתבתי עליו מספר פוסטים בעבר. אם אתם לא מכירים את GraphQL בכלל שווה להתחיל במבוא שכתבתי כאן: שלום GraphQL.

בפוסט היום אני רוצה לכתוב שרת GraphQL ב Node.JS ו Express כדי לראות איך זה עובד. הפרויקט הוא API למערכת ניהול משימות שמאפשר לנו לראות את המשימות הפתוחות במערכת ולעדכן סטטוס "בוצע" של כל משימה.

העליתי את כל הקוד בפוסט לפרויקט דוגמה בגיטהאב בקישור:

https://github.com/ynonp/express-graphql-demo

המשך קריאה

איך שולחים ומקבלים הודעות בתור RabbitMQ מתוך שרת Node.JS Express

21/08/2021

כשאני כותב קוד מדי פעם אני נתקע על שטות שאני חושב שצריכה להיות מאוד ברורה. השטות של היום היתה RabbitMQ ובפרט איך לחבר אותו ל Express. הנה הקוד שבסוף עבד לי בצירוף כמה אילוצים ששווה לשים לב אליהם כשאתם מקודדים דברים דומים.

המשך קריאה

היום למדתי: התקנת חבילות אופליין עם Node.JS

26/10/2019

כשהמקום בדיסק הקשיח שלי מתחיל להיגמר אני עובר עם זכוכית מגדלת על כל הקבצים והתיקיות ומחפש מה אפשר למחוק, כי בתחושה שלי דיסק קשיח של חצי טרה צריך להספיק לפחות לכל החיים. בכל מקרה בתוך החיפוש מצאתי ספריה קטנה בתיקיית הבית שנקראת ~/.npm שתופסת לי היום קצת מעל ג'יגה.

מה שיותר מפתיע היה תוכן התיקיה: היא מכילה את כל החבילות שאי פעם התקנתי ובכל הגירסאות שלהן. זה כאילו מישהו לקח את כל תיקיות ה node_modules של העולם והדביק את הכל יחד לתוך תיקיה אחת. אז הלכתי לתיעוד ומסתבר שזה בדיוק מה ש npm עושה כל פעם שאנחנו מתקינים חבילה. עוד קצת חיטוט בתיעוד ומסתבר שאפשר להשתמש ב cache הזה כדי להפוך את ההתקנות שלכם להרבה יותר מהירות. העברת הדגל --offline ל npm install גורמת ל npm להתקין את הספריה מהמטמון המקומי בלי לבקר באינטרנט כלל.

הנה השוואת זמנים קטנה בשביל לעשות גם לכם חשק:

$ time npm install --save express
real    0m3.625s

$ time npm install --offline --save express
real    0m1.646s

עכשיו רק נשאר למצוא איך להגביל את גודל ספריית ~/.npm כדי שלא תתפוס לי את כל הדיסק.

גם בצד השרת טייפסקריפט עובדת לא רע בכלל

27/01/2019

הקומפיילר של טייפסקריפט מצליח להתמודד יפה גם עם יישומי צד שרת גדולים כולל כאלה שמורכבים ממספר קבצים. מאחר ו TypeScript ו JavaScript עובדות יפה יחד אנחנו יכולים בקלות להוסיף קבצי TypeScript לפרויקט צד שרת קיים. בפוסט זה ניקח יחד את הצעד הראשון.

המשך קריאה

הזמנה לוובינר - אבטחת מידע ב Node.JS

08/01/2019

בשביל לבנות מערכות מאובטחות כדאי שנבין קודם מה יכול להישבר ואיך, כי באבטחת מידע השטן הוא בפרטים. אחד התרגילים שאני אוהב לתת בקורסי פיתוח מאובטח שאני מעביר הוא לתת למשתתפים מערכת קיימת ולשמוע איך הם היו ניגשים לחיפוש פרצות אבטחה בה. דרך החיפוש אנחנו לומדים המון, בין אם מצאנו בעיות ובין אם לא.

בימים הקרובים ועד יום חמישי אני מתכנן תרגיל דומה עם כל מי מכם שירצה להשתתף במשחק. פיתחתי מערכת דוגמא קטנה ב Node.JS עבור לוח מודעות: כל אחד יכול ליצור משתמש במערכת, לפרסם מוצר למכירה ולקנות מוצרים של אחרים. אם פירסמתם מוצר ומישהו קנה אותו אתם מקבלים את הכסף והמוצר נעלם מהלוח.

המערכת לא באוויר אבל הקוד זמין בגיטהאב בקישור הזה:

https://github.com/ynonp/secure-code-livedemo-loby

ביום חמישי בעשר בבוקר ניפגש בזום לדבר על כל בעיות האבטחה במערכת ואיך היינו מתמודדים איתן בעולם האמיתי. עד אז אתם מוזמנים בחום לעבור על הקוד ולמצוא בעיות אבטחה, והאמיצים ביניכם מוזמנים גם לזרוק לי מילה כאן, בטלגרם, או בתחילת הוובינר ותוכלו להציג את מה שמצאתם.

זה דף הוובינר לפרטים והרשמה:

https://www.tocode.co.il/workshops/61

נתראה בחמישי, ינון