সরাসরি পর্বে যান
পর্ব ৪০ · ৫৯-এর মধ্যে · মডিউল ৬

localStorage, sessionStorage, cookie আর IndexedDB — ব্রাউজারে ডেটা রাখা

Browser Storage in JavaScript — localStorage, sessionStorage, the storage Event, Cookies and Their Attributes, IndexedDB, Quotas and Privacy

This Bangla lesson compares the ways a page can remember data: localStorage and its string-only nature with JSON, sessionStorage and the cross-tab storage event, practical patterns for carts, themes, drafts, versioning and expiry, cookies and their attributes like HttpOnly, Secure and SameSite, IndexedDB as an in-browser database, and quotas, security and privacy trade-offs.

এই পর্বে যা শিখবেন

  • ব্রাউজারের প্রতিটি storage-এর আকার, আয়ু, পরিসর আর API — আর কোন কাজে কোনটি বাছবেন
  • localStorage-এ JSON রাখা-পড়া, ভাঙা ডেটা আর বন্ধ storage নিরাপদে সামলানো, সংস্করণ আর মেয়াদের নকশা
  • sessionStorage-এর ট্যাব-ভিত্তিক আচরণ আর storage event দিয়ে কয়েকটি ট্যাব মিলিয়ে রাখা
  • Cookie-র পুরো জীবনচক্র: Set-Cookie, attribute, document.cookie-এর অদ্ভুত API আর third-party cookie-র অবস্থা
  • IndexedDB-র object store, index, transaction আর version; quota, গোপনীয়তা আর AI-এর লেখা storage কোড যাচাই

১ · পাতা কেন সব ভুলে যায় — ব্রাউজারের চারটি তাক

একটি অনলাইন দোকানের কথা ভাবুন। নাদিয়া রাতে মোবাইলে তিনটি বই কার্টে রাখলেন, থিম বদলে “ডার্ক” করলেন, তারপর ঘুমিয়ে পড়লেন। সকালে আবার সাইট খুললেন — কার্ট খালি, থিম আবার সাদা। কারণ JavaScript-এর variable থাকে পাতার মেমরিতে; পাতা বন্ধ বা রিলোড হলে সেই মেমরি পুরো মুছে যায়। প্রতিবার পাতা খোলা মানে নতুন শুরু।

সমাধানের দুটি পথ। এক, ডেটা সার্ভারে পাঠিয়ে ডেটাবেসে রাখা (পর্ব ৩৫-এর fetch দিয়ে) — এতে লগইন লাগে, ইন্টারনেট লাগে। দুই, ডেটা ব্যবহারকারীর নিজের ডিভাইসে, ব্রাউজারের ভেতরে রাখা। এর জন্য ব্রাউজার কয়েকটি জায়গা দেয়, যাদের আকার, আয়ু, পরিসর আর API আলাদা। ঠিক কাজে ঠিক তাক বাছাই এই পর্বের মূল দক্ষতা।

একটি বাসার কথা ভাবুন। দরজার পাশে চাবি রাখার ছোট হুক — বাসার সবাই রোজ দেখেন, ছোট জিনিস রাখা যায় (localStorage). টেবিলের ওপর চিরকুট — আজকের কাজের তালিকা, ঘর ছাড়লেই ফেলে দেন (sessionStorage). মানিব্যাগে রাখা ভোটার আইডি — যেখানেই যান সাথে যায়, প্রতিবার দেখাতে হয় (cookie, প্রতিটি request-এ সার্ভারে যায়)। আর স্টোররুমের বড় আলমারি — তাক, লেবেল আর তালিকা দিয়ে সাজানো, অনেক জিনিস ধরে (IndexedDB). চারটিই বাসার ভেতরে, কিন্তু কাজ আলাদা।

🗝️ localStorage

ছোট সেটিং আর ছোট ডেটা — থিম, ভাষা, কার্ট। ব্রাউজার বন্ধ করলেও থাকে, যতক্ষণ না কেউ মোছে। শুধু string, synchronous API.

উদাহরণ: ডার্ক মোড, ভাষা বাছাই

📝 sessionStorage

একই API, কিন্তু প্রতিটি ট্যাবের আলাদা। রিলোডে টেকে, ট্যাব বন্ধ করলে মুছে যায়। অস্থায়ী, এক ট্যাবের কাজ।

উদাহরণ: কয়েক ধাপের ফর্মের মাঝের অবস্থা

🪪 Cookie

খুব ছোট (প্রায় ৪ KB), আর প্রতিটি request-এর সাথে নিজে থেকে সার্ভারে যায়। সার্ভারও লিখতে পারে, চাইলে JavaScript-এর কাছ থেকে লুকিয়েও।

উদাহরণ: লগইন session

🗄️ IndexedDB

ব্রাউজারের ভেতরের আসল ডেটাবেস — object, Blob, index, transaction. বড় ডেটা আর অফলাইন অ্যাপের জন্য। Asynchronous API.

উদাহরণ: অফলাইন অর্ডারের সারি

সবার ওপরের নিয়ম — origin

পর্ব ৩৫-এ দেখেছেন, ব্রাউজার একটি ঠিকানাকে তিন ভাগে দেখে: scheme (https), host (shop.example.com) আর port (443). তিনটির জোড়ই origin. Web Storage আর IndexedDB পুরোপুরি origin ধরে ভাগ করা — প্রতিটি origin-এর নিজস্ব আলাদা ঘর। এটি same-origin policy-রই অংশ।

https://shop.example.com-এর সাথে তুলনাএকই storage?কারণ
https://shop.example.com/cart?id=7হ্যাঁpath আর query origin-এর অংশ নয়
http://shop.example.comনাscheme আলাদা (http বনাম https)
https://www.example.comনাhost আলাদা — subdomain-ও আলাদা origin
https://shop.example.com:8443নাport আলাদা

Cookie একটু আলাদা — চলে domain আর path ধরে, scheme বা port ধরে নয় (অংশ ৫)। আর মনে গেঁথে নিন: origin-এর দেয়াল অন্য সাইট-কে আটকায়, কিন্তু আপনার পাতায় চলা যেকোনো script — আপনার কোড, analytics, বিজ্ঞাপন, বা XSS দিয়ে ঢোকা দুষ্ট কোড — এই storage পড়তে-লিখতে পারে। ব্যবহারকারীও DevTools খুলে সব দেখতে, বদলাতে, মুছতে পারেন।

মূল ধারণা

ব্রাউজারের storage হলো ব্যবহারকারীর ডিভাইসে রাখা, origin ধরে আলাদা, মুছে যেতে পারে এমন জায়গা। তাই এখানে রাখুন সুবিধার জিনিস (পছন্দ, খসড়া, cache), কিন্তু গোপন তথ্য নয়, আর পড়ার সময় বিশ্বাস করবেন না — যেকোনো সময় ডেটা না থাকতে পারে, পুরোনো ফরম্যাটে থাকতে পারে, বা হাতে বদলানো হতে পারে।

২ · localStorage — string রাখার ছোট আলমারি

localStorage হলো Web Storage-এর স্থায়ী অংশ — একটি key-value তালিকা, যেখানে key আর value দুটোই string. API মাত্র কয়েকটি:

localStorage.setItem("theme", "dark");   // রাখা (থাকলে বদলে দেয়)
localStorage.getItem("theme");           // "dark"
localStorage.getItem("nothing");         // null — key নেই
localStorage.length;                     // কয়টি key আছে
localStorage.key(0);                     // ০ নম্বর key-এর নাম (ক্রমের নিশ্চয়তা নেই)
localStorage.removeItem("theme");        // একটি মোছা
localStorage.clear();                    // এই origin-এর সব মোছা

localStorage.theme = "dark" লিখেও কাজ চলে, কিন্তু এড়িয়ে চলুন — key বা length নামের key রাখলে method-এর সাথে নাম মিলে গোলমাল হয়। সবচেয়ে বড় ফাঁদ হলো string-only নিয়ম: যা দেবেন, ব্রাউজার আগে তাকে string বানাবে। সংখ্যা 3 হয় "3", true হয় "true", array [1, 2] হয় "1,2", object হয় "[object Object]" — ডেটা চিরতরে হারায়। তাই কাঠামোবদ্ধ ডেটার জন্য পর্ব ১৯-এর serialization: রাখার আগে JSON.stringify, পড়ার পর JSON.parse. পুরো যাত্রাটি ধাপে ধাপে দেখুন:

1const cart = { items: [{ id: "p7", qty: 2 }], at: new Date(T0), note: undefined };2const text = JSON.stringify(cart);3localStorage.setItem("cart", text);4// ...ব্যবহারকারী পাতা রিলোড করলেন...5const raw = localStorage.getItem("cart");6const back = JSON.parse(raw);7console.log(typeof back.at, "note" in back);
কোথায় কী আছে
cart (মেমরি)
text (মেমরি)
ডিস্কে: cart
raw / back
  1. মেমরিতে একটি object. কার্টে একটি array, একটি Date আর একটি undefined field — আছে শুধু এই পাতার heap-এ। সরাসরি setItem("cart", cart) লিখলে রাখা হতো শুধু "[object Object]".
  2. Object থেকে JSON string. JSON.stringify কার্টকে লেখা বানাল। দুটি নীরব ক্ষতি খেয়াল করুন: Date হয়ে গেল ISO string, আর note: undefined key-টিই বাদ পড়ল (পর্ব ১৯-এর round trip-এর নিয়ম)। Map বা Set থাকলে হতো {}.
  3. Origin-এর ঘরে রাখা। setItem string-টি এই origin-এর localStorage-এ লিখল — ব্রাউজার একে ডিস্কে জমায়। কাজটি synchronous: লেখা শেষ না হওয়া পর্যন্ত পরের লাইন চলে না। জায়গা ফুরোলে এখানেই QuotaExceededError আসতে পারে।
  4. রিলোড — মেমরি সাফ, ডিস্ক অক্ষত। cart, text — সব variable মুছে গেছে। কিন্তু localStorage-এর string রয়ে গেছে, কম্পিউটার রিস্টার্ট দিলেও থাকবে।
  5. getItem দেয় string. ফেরত এল ঠিক সেই লেখা — object নয়। key না থাকলে আসত null; আর JSON.parse(null) ভুল দেয় না, ফল দেয় null — তাই পরে ?? দিয়ে ডিফল্ট বসানো যায়। কিন্তু কেউ আগে setItem("cart", undefined) লিখে থাকলে রাখা থাকত string "undefined", আর তার parse দিত SyntaxError.
  6. JSON.parse — নতুন object, আংশিক রূপে। Parse সম্পূর্ণ নতুন object বানাল। কিন্তু at এখন string (Date ফেরাতে পর্ব ১৯-এর reviver লাগবে), আর note নেই — তাই আউটপুট string false. আর লেখাটি ভাঙা হলে (পুরোনো সংস্করণ, হাতে বদলানো) এখানে SyntaxError — তাই এই লাইন সবসময় try/catch-এর ভেতরে।

Synchronous আর ছোট — দুটি সীমা

localStorage-এর API synchronous — getItem ফল হাতে দিয়েই ফেরে। লিখতে সহজ, কিন্তু মূল্য আছে: ব্রাউজারকে ডেটা ডিস্ক থেকে পড়ে আনতে হয়, আর সেই সময় main thread আটকে থাকে (পর্ব ৩১)। কয়েক KB-তে কেউ টের পায় না; কিন্তু কয়েক MB-এর JSON প্রতিবার stringify/parse করলে স্ক্রল আর ক্লিক ঝাঁকুনি দেয়। আর Web Worker (পর্ব ৪২) বা service worker (পর্ব ৪৪) থেকে localStorage ব্যবহারই করা যায় না — সেখানে IndexedDB.

আকারের সীমা মানদণ্ডে নির্দিষ্ট নয়; বাস্তবে প্রধান ব্রাউজারগুলো প্রতি origin-এ মোটামুটি ৫ MB-এর কাছাকাছি দেয়। সীমা পেরোলে setItem ছোড়ে QuotaExceededError নামের DOMException. তাই localStorage “ছোট সেটিং”-এর জায়গা, ছবি বা পণ্যের পুরো ক্যাটালগের নয়।

যখন localStorage থাকেই না — নিরাপদ মোড়ক

এবার একটি বাস্তব কথা, যা অনেক টিউটোরিয়াল এড়িয়ে যায়: localStorage শব্দটি পড়তে গেলেই exception আসতে পারে। ব্যবহারকারী ব্রাউজারের সেটিংসে সাইট-ডেটা বন্ধ রেখেছেন, পাতা চলছে কোনো sandbox করা iframe-এ, পুরোনো Safari-র private মোডে quota ছিল শূন্য, বা ডিস্ক ভরা — যেকোনো এক কারণে পুরো অ্যাপ সাদা পাতা হয়ে যেতে পারে। এই কোর্সের কোড চালানোর বাক্সটিও আসলে একটি sandbox করা iframe — তাই নিচের কোড এখানে চালালে আপনি নিজের চোখেই fallback দেখবেন:

function createSafeStorage() {
  try {
    const ls = window.localStorage;          // বন্ধ থাকলে এখানেই exception
    ls.setItem("__probe__", "1"); ls.removeItem("__probe__");   // লেখা যায় কি না
    return { store: ls, kind: "localStorage" };
  } catch (e) {
    console.log("fallback →", e.name + ":", e.message);
    const map = new Map();                   // একই API, শুধু মেমরিতে
    return { kind: "memory", store: {
      getItem: (k) => (map.has(String(k)) ? map.get(String(k)) : null),
      setItem: (k, v) => void map.set(String(k), String(v)),
      removeItem: (k) => void map.delete(String(k)),
      clear: () => map.clear(),
      key: (i) => [...map.keys()][i] ?? null,
      get length() { return map.size; },
    } };
  }
}
const { store, kind } = createSafeStorage();
console.log("ব্যবহার হচ্ছে:", kind);
store.setItem("theme", "dark"); store.setItem("qty", 3);   // দ্বিতীয়টিতে number দিলাম
console.log(store.getItem("qty"), typeof store.getItem("qty"), store.length, store.key(0));
store.setItem("cart", { id: 1 });            // object দিলাম — JSON ছাড়া
console.log(store.getItem("cart"), store.getItem("missing"));

প্রথম লাইনটি Edge-এর আসল বার্তা: fallback → SecurityError: Failed to read the 'localStorage' property from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag. কোড-বাক্সের iframe-এ sandbox="allow-scripts" আছে, কিন্তু allow-same-origin নেই — তাই ব্রাউজার তাকে একটি opaque origin দেয়, যার কোনো storage-এর ঘর নেই। খেয়াল করুন, exception এসেছে setItem-এ নয়, window.localStorage পড়তেই. সাইট-ডেটা বন্ধ থাকলে Chrome/Edge একই নামের error দেয় (বার্তা আলাদা), আর পুরোনো Safari-র private মোডে setItem দিত QuotaExceededError — তাই একটি পরীক্ষামূলক লেখা (__probe__)। তারপর ব্যবহার হচ্ছে: memory — বাকি অ্যাপ টেরও পায় না, শুধু রিলোডে ডেটা টিকবে না। 3 string 2 theme: সংখ্যা ৩ রাখা হয়েছে string "3" হয়ে, ঠিক আসল localStorage-এর মতো। শেষ লাইন [object Object] null — JSON ছাড়া object রাখলে ডেটা হারায়, আর নেই এমন key-তে null. কোডে error-এর বার্তা নয়, e.name দেখে সিদ্ধান্ত নিন।

৩ · sessionStorage আর storage event — ট্যাবের হিসাব

sessionStorage-এর API হুবহু localStorage-এর মতো — setItem, getItem, সব একই, string-only নিয়মও একই। পার্থক্য শুধু পরিসর আর আয়ু-তে:

  • প্রতিটি ট্যাবের নিজের: একই সাইট দুটি ট্যাবে খুললে দুটির sessionStorage আলাদা — এক ট্যাবে লেখা অন্যটি দেখে না।
  • রিলোডে টেকে, ট্যাব বন্ধে মোছে: পাতা রিলোড বা একই ট্যাবে অন্য পাতায় গিয়ে ফিরে এলে ডেটা থাকে। ট্যাব বন্ধ করলে শেষ। (অনেক ব্রাউজার “বন্ধ ট্যাব আবার খুলুন”-এ sessionStorage-ও ফিরিয়ে আনে।)
  • Duplicate করলে কপি: ট্যাবে ডান-ক্লিক করে “Duplicate” চাপলে নতুন ট্যাব তখনকার sessionStorage-এর একটি কপি পায় — তারপর থেকে দুটি আলাদা পথে চলে। সাধারণ লিংক নতুন ট্যাবে খুললে সাধারণত নতুন, খালি sessionStorage.

কোথায় কাজে লাগে? কয়েক ধাপের চাকরির আবেদন ফর্মের মাঝের অবস্থা, বা একটি ট্যাবে চলা পরীক্ষার বর্তমান প্রশ্ন — যে তথ্য শুধু এই ট্যাবের কাজের জন্য। দুই ট্যাবে দুটি আলাদা বুকিং চললে sessionStorage তাদের গুলিয়ে ফেলে না, localStorage ফেলত।

storage event — অন্য ট্যাবকে খবর দেওয়া

ধরুন নাদিয়ার দুটি ট্যাবে একই দোকান খোলা। একটিতে তিনি থিম “ডার্ক” করলেন। অন্য ট্যাবটি কীভাবে জানবে? localStorage বদলালে ব্রাউজার একই origin-এর অন্য সব খোলা পাতায় একটি storage event পাঠায়। মজার নিয়ম হলো, যে পাতা লিখল, সে নিজে event পায় না:

ট্যাব Ashop.example.com — লিখবে
ট্যাব Bshop.example.com — শুনছে
ট্যাব Cblog.example.com — অন্য origin
localStorageorigin: https://shop.example.com
theme: "light" → "dark"ডিস্কে লেখা হলো
storage event → ট্যাব Bkey, oldValue, newValue, url
ট্যাব A ও Cকোনো event নেই
একই মান আবার লেখামান বদলায়নি — event নেই
  1. দুই ট্যাব, একই origin. ট্যাব A আর B দুটোতেই shop.example.com খোলা, আর দুটোতেই window.addEventListener("storage", …) বসানো। ট্যাব C-তে একই কোম্পানির ব্লগ, কিন্তু host আলাদা — তাই আলাদা origin, আলাদা localStorage.
  2. ট্যাব A লিখল। localStorage.setItem("theme", "dark") — মান বদলাল। A নিজের রং তো নিজের কোডেই বদলায়, তাই ব্রাউজার তাকে event পাঠায় না। একই ট্যাবে listener বসিয়ে পরীক্ষা করে অনেকে ভাবেন “event কাজ করছে না”.
  3. ট্যাব B-তে event. ব্রাউজার একই origin-এর অন্য পাতাগুলোর জন্য event সারিতে রাখে। B-এর listener পায় e.key = "theme", e.oldValue = "light", e.newValue = "dark", e.url (কোন পাতা লিখল) আর e.storageArea. clear() হলে e.key হয় null.
  4. B মিলে গেল, C নীরব। B নিজের পাতায় ডার্ক থিম বসাল — নাদিয়া ট্যাব বদলে দেখেন দুটোই একই রঙে। ট্যাব C কোনো event পায়নি, কারণ origin আলাদা। লগআউটেও এই কৌশল চলে: এক ট্যাবে লগআউট করলে বাকি ট্যাবগুলোকে লগইন পাতায় পাঠানো।
  5. একই মান আবার লিখলে — কিছুই না। A যদি আবার setItem("theme", "dark") লেখে, মান তো বদলায়নি — মানদণ্ড অনুযায়ী তখন কোনো event যায় না। আর sessionStorage-এর event যায় শুধু একই ট্যাবের ভেতরের অন্য same-origin iframe-এ, কারণ অন্য ট্যাবের sessionStorage তো আলাদাই।
// প্রতিটি ট্যাবে — অন্য ট্যাবে থিম বা লগইন বদলালে সাড়া দেওয়া
window.addEventListener("storage", (e) => {
  if (e.storageArea !== localStorage) return;
  if (e.key === "theme") document.documentElement.dataset.theme = e.newValue ?? "light";
  if (e.key === "auth:loggedOut") location.assign("/login.html");
});

শুধু “বদলেছে” খবর নয়, ট্যাবগুলোর মধ্যে নিয়মিত বার্তা পাঠাতে আরও পরিষ্কার API আছে — BroadcastChannel. এটি storage-এ কিছু লেখে না, শুধু একই origin-এর অন্য ট্যাবে বার্তা পৌঁছায়।

৪ · বাস্তব নকশা — কার্ট, থিম, খসড়া, সংস্করণ আর মেয়াদ

API জানা সহজ অংশ। কঠিন অংশ ছয় মাস পরের কথা ভাবা: নতুন সংস্করণে কার্টের আকার বদলালে পুরোনো ডিভাইসের পুরোনো ডেটার কী হবে? তাই অভিজ্ঞরা কাঁচা ডেটা রাখেন না, রাখেন একটি “খাম”-এর ভেতরে — সাথে সংস্করণ নম্বর (migration-এর জন্য) আর মেয়াদ (TTL). নিচের কোডে আগের অংশের মেমরি-fallback-এর ছোট রূপ ব্যবহার করা হয়েছে, আর সময় স্থির রাখা হয়েছে যাতে ফল প্রতিবার একই আসে:

const mem = new Map();                       // safeStorage-এর fallback-এর ছোট রূপ
const store = { getItem: (k) => mem.get(k) ?? null, setItem: (k, v) => mem.set(k, String(v)) };
const VERSION = 2, HOUR = 3_600_000;
const T0 = Date.UTC(2026, 9, 19, 4, 0);      // স্থির সময়: ১৯ অক্টোবর ২০২৬, ঢাকায় সকাল ১০টা

function save(key, data, now, ttl) {
  store.setItem(key, JSON.stringify({ v: VERSION, expiresAt: now + ttl, data }));
}
function load(key, now) {
  let box;
  try { box = JSON.parse(store.getItem(key)); } catch { return "ভাঙা ডেটা"; }
  if (box === null) return "নেই";
  if (box.v === 1) box = { v: 2, expiresAt: now + 24 * HOUR, data: { items: box.items } }; // migration
  if (now >= box.expiresAt) return "মেয়াদ শেষ";
  return box.data;
}
save("cart", { items: [{ id: "p7", qty: 2 }], at: new Date(T0), note: undefined }, T0, 24 * HOUR);
console.log(store.getItem("cart"));
console.log(JSON.stringify(load("cart", T0 + 2 * HOUR)));
console.log(load("cart", T0 + 25 * HOUR), "|", load("wishlist", T0));
store.setItem("old", '{"v":1,"items":[{"id":"p1","qty":1}]}');
store.setItem("bad", '{"v":2,"data":');
console.log(JSON.stringify(load("old", T0)), "|", load("bad", T0));

প্রথম লাইনে রাখা string-টি: {"v":2,"expiresAt":1792468800000,"data":{…,"at":"2026-10-19T04:00:00.000Z"}} — খামে সংস্করণ আর মেয়াদ (T0 থেকে ২৪ ঘণ্টা পরের timestamp), Date হয়েছে ISO string, আর note উধাও। দুই ঘণ্টা পরে পড়লে কার্ট ফেরত আসে। ২৫ ঘণ্টা পরে মেয়াদ শেষ, আর কখনো না রাখা wishlist-এ নেই — getItem দিয়েছে null, আর JSON.parse(null) দিয়েছে null. শেষ লাইনে {"items":[{"id":"p1","qty":1}]} | ভাঙা ডেটা: পুরোনো v1 আকারের ডেটা migration হয়ে নতুন আকারে এসেছে, আর অর্ধেক কাটা JSON-এ exception না ছুড়ে নিরাপদ উত্তর। বাস্তবে migration-এর পর নতুন রূপটি আবার save করে রাখুন, যাতে প্রতিবার রূপান্তর করতে না হয়; আর সময় হিসেবে Date.now() দিন — এখানে স্থির T0 শুধু ফল একই রাখার জন্য।

চারটি চেনা নকশা

১. ডার্ক মোড — পাতা আঁকার আগেই। থিম localStorage-এ রাখুন, আর পড়ুন <head>-এর একটি ছোট inline script-এ, যাতে পাতা প্রথমে সাদা দেখিয়ে তারপর কালো না হয় (এই ঝলকানিকে বলে “flash of wrong theme”). কিছু রাখা না থাকলে ব্যবহারকারীর সিস্টেমের পছন্দ মানুন:

let saved = null;
try { saved = localStorage.getItem("theme"); } catch {}   // storage বন্ধ থাকলেও পাতা চলবে
const dark = saved ? saved === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.dataset.theme = dark ? "dark" : "light";

২. লম্বা অভিযোগের ফর্ম — খসড়া নিজে থেকে সেভ। কেউ দশ মিনিট ধরে অভিযোগ লিখলেন, তারপর লোডশেডিংয়ে ল্যাপটপ বন্ধ — সব লেখা গেল। সমাধান: টাইপ থামার আধা সেকেন্ড পরে খসড়া রাখা — পর্ব ৩৬-এর debounce. Submit হলে খসড়া মুছুন। পাসওয়ার্ড, কার্ড বা NID নম্বরের field খসড়ায় রাখবেন না।

const form = document.querySelector("#complaint");
const KEY = "draft:complaint";
let timer;
form.addEventListener("input", () => {
  clearTimeout(timer);
  timer = setTimeout(() => {
    const data = Object.fromEntries(new FormData(form));   // পর্ব ৩৯
    try { localStorage.setItem(KEY, JSON.stringify(data)); } catch { /* জায়গা নেই — চুপচাপ বাদ */ }
  }, 500);
});
form.addEventListener("submit", () => localStorage.removeItem(KEY));

৩. “সম্প্রতি দেখা পণ্য” — সর্বোচ্চ ১০টি। নতুন পণ্য সামনে, পুরোনো একই পণ্য থাকলে সরিয়ে দিন, আর তালিকা ১০-এ কেটে দিন — নইলে বছরের পর বছর বাড়তেই থাকবে:

function addRecent(id) {
  let list = [];
  try { list = JSON.parse(localStorage.getItem("recent")) ?? []; } catch {}
  if (!Array.isArray(list)) list = [];                       // কেউ হাতে বদলালেও টিকবে
  const next = [id, ...list.filter((x) => x !== id)].slice(0, 10);
  localStorage.setItem("recent", JSON.stringify(next));
  return next;
}
// addRecent("p3") → ["p3"];  তারপর addRecent("p9") → ["p9","p3"];  আবার addRecent("p3") → ["p3","p9"]

৪. কার্ট — কিন্তু সত্যের উৎস সার্ভার। লগইন ছাড়া কার্ট localStorage-এ রাখা প্রচলিত, কিন্তু ব্যবহারকারী DevTools খুলে price বদলে ১ টাকা করতে পারেন। তাই রাখুন শুধু পণ্যের ID আর পরিমাণ; দাম, স্টক আর ছাড় চেকআউটে সার্ভারেই হিসাব হবে।

৫ · Cookie — প্রতিটি request-এ সাথে যাওয়া চিরকুট

Web Storage আসার প্রায় ১৫ বছর আগে থেকে ব্রাউজারে ডেটা রাখার একমাত্র উপায় ছিল cookie. কিন্তু cookie মূলত JavaScript-এর জন্য বানানো হয়নি — বানানো হয়েছিল সার্ভারের জন্য। HTTP নিজে কিছু মনে রাখে না: প্রতিটি request আলাদা, সার্ভার জানে না এটি একই ক্রেতা কি না। Cookie সেই সমস্যার সমাধান — সার্ভার উত্তরের সাথে Set-Cookie header (পর্ব ৩৫-এর HTTP header) পাঠায়, ব্রাউজার তা জমায়, আর পরের প্রতিটি request-এ Cookie header-এ ফেরত পাঠায়। পুরো জীবনচক্র দেখুন:

  1. সার্ভারSet-Cookieলগইন সফল
  2. Cookie jarsid, langব্রাউজার জমাল
  3. পরের requestCookie:নিজে থেকে গেল
  4. JavaScriptdocument.cookieHttpOnly লুকানো
লগইনের উত্তরসার্ভার → ব্রাউজার
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: sid=8f3a9c…; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1209600
Set-Cookie: lang=bn; Path=/; Max-Age=31536000; SameSite=Lax
ব্রাউজারের cookie jarshop.example.com
নাম   মান        Domain              Path  মেয়াদ        পতাকা
sid   8f3a9c…    shop.example.com    /     ১৪ দিন পরে    Secure HttpOnly Lax
lang  bn         shop.example.com    /     ১ বছর পরে     Lax
পরের প্রতিটি requestব্রাউজার → সার্ভার
GET /cart HTTP/1.1
Host: shop.example.com
Cookie: sid=8f3a9c…; lang=bn

GET /images/logo.png HTTP/1.1      ← ছবির request-এও একই cookie যায়
Cookie: sid=8f3a9c…; lang=bn
পাতার JavaScript (DevTools console)HttpOnly-র ফল
> document.cookie
'lang=bn'          ← sid নেই! script দেখতেই পায় না

> document.cookie = "sid=hacked"
HttpOnly cookie script দিয়ে বদলানোও যায় না
অন্য সাইট থেকে আসা requestSameSite=Lax
evil.example থেকে shop.example.com-এ লুকানো POST ফর্ম
  → sid পাঠানো হয় না (cross-site POST)

অন্য সাইটের লিংকে ক্লিক করে shop.example.com-এ যাওয়া (GET)
  → sid পাঠানো হয় — ক্রেতা লগইন অবস্থায় পৌঁছান
মেয়াদ শেষ বা লগআউটসার্ভার → ব্রাউজার
লগআউটের উত্তর:
Set-Cookie: sid=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax
  → ব্রাউজার sid মুছে ফেলে

অথবা ১৪ দিন পার → মেয়াদ শেষ, ব্রাউজার নিজেই মোছে
(সার্ভারেও session বাতিল করতে হবে — শুধু cookie মোছা যথেষ্ট নয়)
  1. সার্ভার cookie পাঠাল। পাসওয়ার্ড মিলেছে, সার্ভার একটি এলোমেলো session ID বানিয়ে নিজের ডেটাবেসে রাখল, আর উত্তরে দুটি Set-Cookie header দিল। প্রতিটি header-এ একটি cookie: আগে নাম=মান, তারপর ; দিয়ে আলাদা করা attribute.
  2. ব্রাউজার jar-এ রাখল। ব্রাউজার প্রতিটি cookie-কে নাম, মান আর attribute-সহ নিজের cookie jar-এ জমাল। Max-Age দেওয়া আছে বলে এরা ব্রাউজার বন্ধ করলেও থাকবে (persistent cookie); Max-Age বা Expires না থাকলে হতো session cookie — ব্রাউজার বন্ধে মোছে।
  3. প্রতিটি request-এ নিজে থেকে। এখন shop.example.com-এ যেকোনো request — পাতা, API, এমনকি ছবি — সবগুলোর সাথে ব্রাউজার Cookie header জুড়ে দেয়, কোনো JavaScript ছাড়াই। খরচ: cookie বড় হলে প্রতিটি request বড় হয়।
  4. HttpOnly — script-এর চোখের আড়ালে। document.cookie শুধু lang=bn দেখায়; sid-এ HttpOnly আছে, তাই কোনো script তা পড়তে বা বদলাতে পারে না। XSS দিয়ে দুষ্ট script ঢুকলেও session ID চুরি করে অন্য কম্পিউটারে নিতে পারবে না।
  5. SameSite — অন্য সাইটের request. অন্য সাইটের লুকানো ফর্ম আপনার নামে দোকানে POST করতে চাইলে (CSRF), SameSite=Lax থাকায় ব্রাউজার sid পাঠায় না। কিন্তু অন্য সাইটের লিংকে ক্লিক করে আসা সাধারণ GET-এ পাঠায় — নইলে ক্রেতা প্রতিবার লগআউট অবস্থায় পৌঁছাতেন। Strict সেটাও আটকায়; None সব জায়গায় পাঠায় (আর তখন Secure বাধ্যতামূলক)।
  6. শেষ — মেয়াদ বা লগআউট। একই নাম, Path আর Domain দিয়ে Max-Age=0 পাঠালে ব্রাউজার cookie মুছে দেয়। ১৪ দিন পেরোলেও নিজে মোছে। কিন্তু নিরাপদ লগআউটে সার্ভারের দিকেও session বাতিল করতে হয় — নইলে কেউ আগে কপি করে রাখা ID দিয়ে ঢুকতে পারত।

Attribute-গুলো এক নজরে

Attributeকী করেমনে রাখার কথা
Expires / Max-Ageকখন মুছবে — নির্দিষ্ট তারিখ, বা এত সেকেন্ড পরেদুটোই থাকলে Max-Age জেতে; কোনোটি না থাকলে session cookie. Chrome সর্বোচ্চ মেয়াদ ৪০০ দিনে বেঁধে দেয়
Pathকোন path-এর নিচের request-এ যাবেনিরাপত্তার দেয়াল নয় — একই origin-এর অন্য path-এর script-ও উপায় খুঁজে পড়তে পারে
Domainকোন host-এ যাবেনা দিলে শুধু ঠিক সেই host. Domain=example.com দিলে সব subdomain-এও যায় — পরিসর বাড়ে, ঝুঁকিও
Secureশুধু HTTPS-এ পাঠাবেলগইন cookie-তে সবসময়; SameSite=None-এর জন্য বাধ্যতামূলক
HttpOnlyJavaScript পড়তে বা লিখতে পারবে নাশুধু সার্ভার Set-Cookie দিয়ে বসাতে পারে; session token-এর জন্য আদর্শ
SameSiteঅন্য সাইট থেকে শুরু হওয়া request-এ যাবে কি নাStrict / Lax / None; কিছু না লিখলে Chrome ২০২০ থেকে Lax ধরে নেয়

document.cookie — বিশ্বের অদ্ভুততম API-গুলোর একটি

JavaScript থেকে cookie-র পুরোনো API document.cookie — একটি property, যা পড়লে আর লিখলে দুই রকম আচরণ করে। পড়লে পান সব (HttpOnly ছাড়া) cookie একটি string-এ: "lang=bn; cartId=C-9" — শুধু নাম আর মান, attribute নয়। লিখলে পুরোটা বদলায় না, বরং একটি cookie যোগ বা বদল হয়। মুছতে হয় মেয়াদ শেষ করে, আর তখন Path ও Domain হুবহু মেলাতে হয় — নইলে নীরবে কিছুই হয় না।

// লেখা — প্রতিবার একটি cookie; মান encode করুন
document.cookie = "lang=" + encodeURIComponent("bn") + "; Path=/; Max-Age=31536000; SameSite=Lax; Secure";

// পড়া — split(";") বা split("=") সরাসরি ব্যবহার করবেন না
function getCookie(name) {
  for (const part of document.cookie.split("; ")) {
    const i = part.indexOf("=");                       // মানের ভেতরেও "=" থাকতে পারে
    if (part.slice(0, i) === name) return decodeURIComponent(part.slice(i + 1));
  }
  return null;
}

// মোছা — একই Path দিয়ে মেয়াদ শূন্য
document.cookie = "lang=; Path=/; Max-Age=0";

সাধারণ ভুলগুলো: split(";") করলে প্রতিটি অংশের শুরুতে একটি ফাঁকা থেকে যায়, তাই " lang" আর "lang" মেলে না; split("=") করলে base64 মানের শেষের == কেটে যায়; আর আলাদা Path-এ একই নামের দুটি cookie থাকলে দুটোই আসে, কোনটি কার তা বোঝার উপায় নেই। তাই নতুন একটি API এসেছে — Cookie Store API: await cookieStore.get("lang") একটি object দেয় (নাম, মান, মেয়াদ, domain…), কাজ করে Promise দিয়ে, আর service worker-এও চলে। Chromium-ভিত্তিক ব্রাউজারে এটি ২০২০ সাল থেকে আছে, অন্য ব্রাউজারে সমর্থন সাম্প্রতিক — ব্যবহারের আগে "cookieStore" in window দিয়ে যাচাই করুন।

Third-party cookie — বিজ্ঞাপন, ট্র্যাকিং আর বদলে যাওয়া পরিকল্পনা

আপনি news.example পড়ছেন, আর পাতায় একটি বিজ্ঞাপন-কোম্পানির iframe বা ছবি আছে ads.tracker.example থেকে। সেই request-এর সাথে ads.tracker.example-এর cookie গেলে, তা হলো third-party cookie — আপনি যে সাইটে আছেন তার নয়, তৃতীয় পক্ষের। একই tracker হাজারো সাইটে থাকলে সে একই cookie দিয়ে আপনাকে সাইট থেকে সাইটে অনুসরণ করতে পারে। এই কারণে Safari (২০২০ সাল থেকে পুরোপুরি) আর Firefox (Total Cookie Protection, ডিফল্টে ২০২২ থেকে) third-party cookie আটকায় বা প্রতিটি সাইটের জন্য আলাদা করে রাখে।

Chrome-এর গল্প আঁকাবাঁকা। ২০২০ সালে Google ঘোষণা দেয়, দুই বছরের মধ্যে third-party cookie তুলে দেবে; কয়েকবার পিছিয়ে ২০২৪ সালের শুরুতে মাত্র ১% ব্যবহারকারীর জন্য বন্ধ করে পরীক্ষা চালায়। ২০২৪ সালের জুলাইয়ে পরিকল্পনা বদলায় — পুরো বন্ধ না করে ব্যবহারকারীকে বেছে নিতে দেওয়ার কথা বলে, আর ২০২৫ সালে জানায় আলাদা নতুন prompt-ও আনবে না। অর্থাৎ এই লেখার সময় Chrome-এ ডিফল্টে third-party cookie চালু, অন্য প্রধান ব্রাউজারে আটকানো। নীতি আবার বদলাতে পারে — তাই নিজের অ্যাপ এমনভাবে বানান যেন third-party cookie ছাড়াই চলে। পাশাপাশি ব্রাউজারগুলো iframe-এর storage-কে উপরের সাইট ধরে আলাদা রাখছে (storage partitioning) — একই tracker দুটি সাইটে দুটি আলাদা localStorage পায়।

৬ · IndexedDB — ব্রাউজারের ভেতরের ডেটাবেস

একটি অ্যাপ ভাবুন, যা দিয়ে বিক্রয়কর্মী গ্রামের বাজারে অর্ডার নেন — নেটওয়ার্ক কখনো আছে, কখনো নেই। কয়েক হাজার পণ্যের ছবিসহ ক্যাটালগ ডিভাইসে রাখতে হবে, নেট ছাড়াও অর্ডার নিতে হবে, নেট ফিরলে পাঠাতে হবে। localStorage-এর ৫ MB, string-only আর synchronous API — তিনটিই এখানে বাধা। এই কাজের জন্যই IndexedDB.

  • Object store: SQL-এর টেবিলের মতো, তবে সারি নয়, পুরো JavaScript object রাখে — Date, Map, array, এমনকি Blob (ছবি, PDF). রাখার সময় পর্ব ১৯-এর structured clone হয়, JSON নয় — তাই Date থাকে Date (তবে function রাখা যায় না)।
  • Key: প্রতিটি record-এর অনন্য চাবি — object-এর একটি field (keyPath, যেমন "id") বা নিজে থেকে বাড়া সংখ্যা (autoIncrement).
  • Index: অন্য field দিয়ে দ্রুত খোঁজার ব্যবস্থা — যেমন status দিয়ে “সব pending অর্ডার”, বা দামের একটি সীমার পণ্য।
  • Transaction: প্রতিটি পড়া-লেখা একটি transaction-এর ভেতরে — হয় সব কাজ একসাথে সফল, নয়তো কোনোটিই নয় — অর্ডার রাখা আর স্টক কমানোর অর্ধেক কখনো ডিস্কে থাকবে না।
  • Asynchronous: ফল আসে পরে event-এ (onsuccess, onerror); main thread আটকায় না, worker থেকেও চলে।

API-টি ২০১০-এর দশকের শুরুর নকশা — Promise তখনো ভাষায় আসেনি (এসেছে ES2015-এ), তাই সব event দিয়ে। একটি অর্ডার রাখার পুরো প্রবাহ ধাপে ধাপে:

1const req = indexedDB.open("shop", 2);2req.onupgradeneeded = (e) => {3  const os = req.result.createObjectStore("outbox", { keyPath: "id" });4  os.createIndex("byStatus", "status");5};6req.onsuccess = () => {7  const tx = req.result.transaction("outbox", "readwrite");8  tx.objectStore("outbox").put({ id: "o-101", status: "pending", total: 63500 });9  tx.oncomplete = () => console.log("অর্ডার জমা আছে");10};
ভেতরের অবস্থা
DB version
object store
transaction
outbox-এ
  1. খোলার অনুরোধ — ফল পরে। indexedDB.open("shop", 2) সঙ্গে সঙ্গে database দেয় না, দেয় একটি request object. আসল কাজ ব্রাউজার পটভূমিতে করে। ডিভাইসে এখন version 1-এর database, যাতে শুধু products store.
  2. onupgradeneeded — কাঠামো বদলানোর একমাত্র সুযোগ। চাওয়া version (2) রাখা version (1)-এর চেয়ে বড়, তাই ব্রাউজার একটি বিশেষ “versionchange” transaction চালায়। শুধু এখানেই store আর index বানানো বা মোছা যায়। e.oldVersion দেখে ধাপে ধাপে migration করুন। অন্য ট্যাবে পুরোনো version খোলা থাকলে upgrade আটকে onblocked আসে।
  3. onsuccess — database হাতে। Upgrade শেষ, এখন req.result হলো খোলা database. পুরো অ্যাপে একবার খুলে একই connection বারবার ব্যবহার করাই ভালো।
  4. Transaction শুরু। কোন store আর কোন মোডে — "readonly" না "readwrite" — আগেই বলতে হয়। একই store-এ একাধিক readonly একসাথে চলতে পারে; readwrite চলে পালা করে। এতেই দুই ট্যাবের লেখা একে অপরকে নষ্ট করে না।
  5. put — সারিতে রাখা, এখনো ডিস্কে নয়। Object-টি structured clone হয়ে transaction-এর কাজের তালিকায় উঠল (put থাকলে বদলায়, না থাকলে যোগ করে; add থাকলে error দেয়)। কোনো request-এ error হলে পুরো transaction বাতিল (abort) হয়, আগের কাজও ফিরে যায়।
  6. oncomplete — সত্যিই জমা। Transaction-এর হাতে আর কোনো অপেক্ষমাণ request না থাকলে সে নিজে থেকেই commit হয়, তারপর complete event. শুধু এখনই নিশ্চিত করে বলা যায় “অর্ডার জমা আছে”. এই auto-commit-এর একটি বড় ফাঁদ নিচে দেখুন।

Promise-এ মুড়ে নেওয়া — অফলাইন অর্ডারের সারি

Event-ভিত্তিক API বড় কোডে দ্রুত জট পাকায়, তাই বাস্তবে সবাই একে Promise-এ মোড়েন (পর্ব ৩৩–৩৪)। নিচের কোড একটি “outbox” বানায়: নেট না থাকলে অর্ডার এখানে জমে, নেট ফিরলে পাঠানো হয়। এটি পাতার sandbox-এ চলবে না — যেকোনো সাধারণ ওয়েবসাইট খুলে DevTools console-এ (F12) পেস্ট করে দেখুন, তারপর Application ট্যাবের IndexedDB অংশে ডেটা দেখতে পাবেন:

function openDB() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open("shop", 2);
    req.onupgradeneeded = (e) => {
      const db = req.result;
      if (e.oldVersion < 1) db.createObjectStore("products", { keyPath: "id" });
      if (e.oldVersion < 2) db.createObjectStore("outbox", { keyPath: "id" })
                              .createIndex("byStatus", "status");
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
    req.onblocked = () => reject(new Error("অন্য ট্যাবে পুরোনো সংস্করণ খোলা"));
  });
}
const done = (tx) => new Promise((res, rej) => {
  tx.oncomplete = () => res(); tx.onerror = tx.onabort = () => rej(tx.error);
});

async function queueOrder(order) {                     // নেট না থাকলেও কাজ করে
  const db = await openDB();
  const tx = db.transaction("outbox", "readwrite");
  tx.objectStore("outbox").put({ ...order, status: "pending", createdAt: new Date() });
  await done(tx);
}
async function pendingOrders() {
  const db = await openDB();
  const req = db.transaction("outbox").objectStore("outbox").index("byStatus").getAll("pending");
  return new Promise((res, rej) => { req.onsuccess = () => res(req.result); req.onerror = () => rej(req.error); });
}

await queueOrder({ id: "o-101", items: [{ id: "p7", qty: 2 }], total: 63500 });
console.log(await pendingOrders());
// প্রত্যাশিত আউটপুট: [ { id: "o-101", items: [...], total: 63500, status: "pending", createdAt: Date } ]

সবচেয়ে চেনা IndexedDB-বাগ: transaction খোলা রেখে মাঝখানে await fetch(…) করা। Transaction তখনই নিজে commit হয়ে যায়, যখন তার হাতে কোনো অপেক্ষমাণ request থাকে না আর চলতি task শেষ হয়। নেটওয়ার্কের অপেক্ষার মধ্যে সেটাই ঘটে, ফলে পরের put দেয় TransactionInactiveError. নিয়ম: আগে IndexedDB থেকে পড়ে transaction শেষ করুন, তারপর নেটওয়ার্ক, তারপর নতুন transaction-এ “sent” চিহ্ন বসান। এই ঝামেলা কমাতে Jake Archibald-এর ছোট idb library পুরো API-কে Promise-এ মুড়ে দেয়; আরও উঁচু স্তরে আছে Dexie.js.

Cache API আর জায়গার হিসাব

আরেকটি storage আছে, যা পরের দিকে বিস্তারিত দেখবেন: Cache API. এটি HTTP request আর response জোড়া রাখে — caches.open("v1"), তারপর cache.put(request, response). পাতার HTML, CSS, ছবি অফলাইনে দেখাতে service worker এটি ব্যবহার করে (পর্ব ৪৪)। নিয়ম মোটামুটি: ফাইল আর API-র উত্তর Cache API-তে, অ্যাপের ডেটা IndexedDB-তে।

IndexedDB আর Cache API মিলে এক origin কত জায়গা পায়? মানদণ্ড একে বলে storage quota, আর তা ব্রাউজার আর ডিস্কের খালি জায়গার ওপর নির্ভর করে — অনেক ক্ষেত্রে কয়েক GB পর্যন্ত। আন্দাজ না করে জিজ্ঞেস করুন (এটিও sandbox-এ চলবে না, console-এ চালান):

const { usage, quota } = await navigator.storage.estimate();
console.log(`${(usage / 1e6).toFixed(1)} MB ব্যবহার, সীমা প্রায় ${(quota / 1e9).toFixed(1)} GB`);

const persisted = await navigator.storage.persist();   // "জায়গা কম হলে আমার ডেটা মুছো না"
console.log(persisted);                                // true বা false — ব্রাউজারের সিদ্ধান্ত
// প্রত্যাশিত আউটপুট (ডিভাইসভেদে আলাদা): "0.3 MB ব্যবহার, সীমা প্রায় 120.5 GB" আর true/false

ডিফল্টে সব storage “best-effort” — ডিস্কে জায়গা ফুরোলে ব্রাউজার কম ব্যবহৃত origin-এর ডেটা না জানিয়েই মুছতে পারে। persist() অনুরোধ করে ডেটাকে persistent করতে; রাজি হবে কি না, ব্রাউজার সাইটের ব্যবহার দেখে ঠিক করে। তাই মূল্যবান ডেটার একমাত্র কপি কখনো ব্রাউজারে নয় — সুযোগ পেলেই সার্ভারে পাঠান।

৭ · নিরাপত্তা, গোপনীয়তা আর পুরো তুলনা

Token কোথায় রাখবেন — একটি পুরোনো বিতর্ক

লগইনের পর সার্ভার অনেক সময় একটি token দেয় — প্রায়ই JWT. বহু টিউটোরিয়াল (আর AI) লেখে localStorage.setItem("token", jwt), তারপর প্রতিটি fetch-এ Authorization header-এ পাঠায়। সমস্যা হলো, পাতায় একবার XSS ঘটলে (পর্ব ৪৩) দুষ্ট script এক লাইনে token পড়ে বাইরে পাঠাতে পারে, আর মেয়াদ থাকা পর্যন্ত অন্য কম্পিউটার থেকে আপনার নামে কাজ চলে।

প্রচলিত সুপারিশ: session-এর পরিচয় রাখুন HttpOnly; Secure; SameSite=Lax (বা Strict) cookie-তে. Script তা পড়তেই পারে না, তাই চুরির পথ বন্ধ। তবে XSS থাকলে দুষ্ট script ব্যবহারকারীর পাতা থেকেই request পাঠাতে পারে, আর cookie নিজে থেকে যায় বলে CSRF-এর দিকেও নজর লাগে। তাই আসল শিক্ষা: XSS ঠেকানোই প্রথম কাজ; storage-এর সিদ্ধান্ত ক্ষতির পরিমাণ কমায়, ঝুঁকি শূন্য করে না। বিস্তারিত — XSS, CSP আর cookie-র নিরাপত্তা — পর্ব ৪৩-এ।

আর storage থেকে পড়া সব ডেটাকে ব্যবহারকারীর ইনপুটের মতোই দেখুন: দাম বা admin অনুমতি সার্ভারে যাচাই করুন, আর পাতায় দেখাতে textContent ব্যবহার করুন (পর্ব ৩৭)।

গোপনীয়তা — ITP, মুছে ফেলা আর সম্মতি

ব্রাউজারগুলো এখন গোপনীয়তা রক্ষায় storage-এর ওপর নিজেরাই নিয়ম বসায়। সবচেয়ে কড়া Safari-র ITP (Intelligent Tracking Prevention, ২০১৭ থেকে)। ২০২০ সাল থেকে Safari-তে script দিয়ে লেখা storage — localStorage, IndexedDB, document.cookie-এর cookie ইত্যাদি — মুছে যেতে পারে, যদি ৭ দিন Safari ব্যবহারের মধ্যে ব্যবহারকারী সাইটে এসে একবারও কিছুতে ক্লিক বা ট্যাপ না করেন (হোম স্ক্রিনে যোগ করা ওয়েব অ্যাপে নিয়ম আলাদা)। অর্থাৎ iPhone-এ localStorage “চিরস্থায়ী” নয়। সাধারণ ক্ষেত্রে সার্ভারের header দিয়ে বসানো cookie এই সীমার বাইরে।

ব্যবহারকারী নিজেও যেকোনো সময় সাইট-ডেটা মুছতে পারেন, private বা incognito জানালা বন্ধ হলেই সব মোছে, আর সার্ভার লগআউটে Clear-Site-Data header পাঠিয়ে ঐ origin-এর ডেটা মুছতে বলতে পারে। আইনের দিকও আছে: ইউরোপের GDPR (২০১৮) আর ePrivacy নিয়মে ব্যবহারকারীকে চেনা বা ট্র্যাক করার মতো cookie বা storage-এর আগে সাধারণত সম্মতি লাগে — তাই এত সাইটে “cookie banner”. সাইট চালানোর জন্য একেবারে দরকারি জিনিস (কার্ট, লগইন) সাধারণত ছাড় পায়।

পুরো তুলনা

বিষয়localStoragesessionStorageCookieIndexedDB
আকার (সাধারণত)প্রায় ৫ MB / originপ্রায় ৫ MB / originপ্রায় ৪ KB / cookieবড় — ডিস্কের ওপর নির্ভর (প্রায়ই GB)
আয়ুমোছা না পর্যন্তট্যাব বন্ধ না পর্যন্তExpires/Max-Age পর্যন্ত, নইলে ব্রাউজার বন্ধ পর্যন্তমোছা না পর্যন্ত
পরিসরoriginorigin + ট্যাবdomain + pathorigin
APIsynchronous, stringsynchronous, stringdocument.cookie string / Cookie Store APIasynchronous, object ও Blob, transaction
সার্ভারে যায়?নানাহ্যাঁ, প্রতিটি request-এনা
Worker থেকে?নানাCookie Store API দিয়ে (যেখানে সমর্থিত)হ্যাঁ
JavaScript থেকে লুকানো যায়?নানাহ্যাঁ — HttpOnlyনা
কিসের জন্যপছন্দ, থিম, ছোট কার্ট, খসড়াএক ট্যাবের অস্থায়ী অবস্থালগইন session, সার্ভারের দরকারি ছোট তথ্যঅফলাইন ডেটা, বড় তালিকা, ফাইল
C/C++/Python/Java জানলে

C বা Python-এ ডেটা টিকিয়ে রাখতে আপনি ফাইল খোলেন (C-র ফাইল I/O) বা SQLite-এর মতো embedded database ব্যবহার করেন (Python-এ SQLite আর transaction). ব্রাউজারে নিরাপত্তার কারণে সরাসরি ডিস্কের ফাইল ছোঁয়া যায় না; localStorage অনেকটা একটি ছোট key-value ফাইলের মতো, আর IndexedDB হলো SQL ছাড়া একটি embedded database — transaction-এর ধারণা SQLite-এর মতোই, কিন্তু query লিখতে হয় key আর index দিয়ে। বড় পার্থক্য: এই “ফাইল” ব্যবহারকারী বা ব্রাউজার যেকোনো সময় মুছে দিতে পারে।

পরীক্ষা ও ইন্টারভিউতে
  • localStorage, sessionStorage আর cookie-র পার্থক্য? — localStorage স্থায়ী, sessionStorage ট্যাব বন্ধে মোছে, দুটোই প্রায় ৫ MB আর সার্ভারে যায় না; cookie প্রায় ৪ KB, প্রতিটি request-এ সার্ভারে যায়, মেয়াদ attribute দিয়ে ঠিক হয়।
  • localStorage.setItem("u", { a: 1 }) করলে কী রাখা হয়? — "[object Object]"; object রাখতে JSON.stringify লাগে।
  • HttpOnly cookie কী, কেন? — যে cookie JavaScript পড়তে পারে না; XSS হলেও session ID চুরি ঠেকায়।
  • storage event কোথায় fire হয়? — একই origin-এর অন্য পাতায় (ট্যাবে), যে পাতা লিখল সেখানে নয়।
  • IndexedDB কখন localStorage-এর বদলে? — বড় বা কাঠামোবদ্ধ ডেটা, Blob, index দিয়ে খোঁজা, অফলাইন অ্যাপ বা worker থেকে ব্যবহার — কারণ এটি asynchronous আর transaction-ভিত্তিক।

৮ · AI-এর যুগে — storage কোডে AI-এর চেনা ভুল

“লগইন বানাও” বা “কার্ট সেভ করো” বললে AI প্রায় সবসময় localStorage ধরে নেয় — সবচেয়ে সহজ, আর অসংখ্য টিউটোরিয়ালে তাই লেখা। ডেমোতে কোড চমৎকার চলে; সমস্যা দেখা দেয় বাস্তবে: Safari-তে এক সপ্তাহ পরে ডেটা নেই, storage বন্ধ, পুরোনো ফরম্যাটে সাদা পাতা, বা token চুরির পথ।

AI যা প্রায়ই লেখেসমস্যাযা চাইবেন
localStorage.setItem("token", jwt), কখনো পাসওয়ার্ডওXSS হলে এক লাইনে চুরি; শেয়ার করা কম্পিউটারে পরের জন দেখেনHttpOnly + Secure + SameSite cookie; পাসওয়ার্ড কোথাও নয়
JSON.parse(localStorage.getItem(k)) — try/catch ছাড়াভাঙা বা পুরোনো ডেটায় পুরো অ্যাপ থামেtry/catch, আকার যাচাই, ডিফল্ট মান
localStorage-এ পুরো ক্যাটালগ বা ছবি (base64)৫ MB-এ QuotaExceededError; বড় parse-এ main thread আটকায়IndexedDB বা Cache API
document.cookie.split(";") আর split("=")শুরুর ফাঁকায় নাম মেলে না; মানের = কেটে যায়; decode নেই"; " দিয়ে ভাগ, প্রথম =-এ কাটা, decodeURIComponent
Storage-এর অস্তিত্ব ধরে নেওয়াবন্ধ storage বা sandbox-এ SecurityErrortry/catch-এ মোড়া wrapper আর মেমরি fallback
সংস্করণ বা মেয়াদ ছাড়া ডেটাঅ্যাপ বদলালে পুরোনো ডেটা ভুল পড়া; বাসি দামখামে v আর expiresAt, migration
IndexedDB transaction-এর ভেতরে await fetchTransactionInactiveErrorপড়া → নেটওয়ার্ক → নতুন transaction

AI-কে storage-এর কোড লিখতে বললে কোন ডেটা কোথায় যাবে, সেটা নিজে ঠিক করে দিন আর ব্যর্থতার ক্ষেত্রগুলো নাম ধরে বলুন:

আমি একটি অনলাইন দোকানের ফ্রন্ট-এন্ড বানাচ্ছি (সাধারণ JavaScript, কোনো framework নয়)। নিচের শর্ত মেনে একটি storage.js মডিউল লিখে দাও: ১. createSafeStorage(): localStorage ব্যবহারের চেষ্টা করবে try/catch-এর ভেতরে; SecurityError বা QuotaExceededError হলে একই API-র (getItem, setItem, removeItem, clear, key, length) মেমরি-ভিত্তিক fallback দেবে। ২. saveJSON(key, data, ttlMs) আর loadJSON(key, fallback): ডেটা রাখবে { v, expiresAt, data } খামে; parse ব্যর্থ, মেয়াদ শেষ বা অচেনা সংস্করণ হলে fallback ফেরত দেবে, exception ছুড়বে না; v: 1 থেকে v: 2-তে একটি migration দেখাবে। ৩. কার্টে শুধু পণ্যের id আর qty রাখবে — দাম নয়। "সম্প্রতি দেখা" তালিকা সর্বোচ্চ ১০টি, পুনরাবৃত্তি ছাড়া। ৪. কোনো token, পাসওয়ার্ড বা ব্যক্তিগত তথ্য localStorage-এ রাখবে না; লগইন session যে HttpOnly, Secure, SameSite=Lax cookie-তে সার্ভার রাখবে, সেটা মন্তব্যে লিখবে। ৫. অন্য ট্যাবে কার্ট বদলালে storage event দিয়ে এই ট্যাবের কার্ট হালনাগাদ করবে। শেষে ৫টি পরীক্ষার কেস দাও: storage বন্ধ, ভাঙা JSON, মেয়াদ শেষ, v: 1-এর পুরোনো ডেটা, আর কেউ হাতে "recent"-এ একটি string রেখে দিয়েছে — প্রতিটির প্রত্যাশিত ফলসহ।

AI-এর লেখা storage কোড — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১১৯৯৪ সালে Netscape-এর এক তরুণ প্রোগ্রামার একটি শপিং কার্টের সমস্যা মেটাতে cookie বানালেন। কেন HTTP-র এমন একটি জিনিস দরকার হলো, আর সেই তাড়াহুড়োর নকশা কেন আজও আমাদের ভোগায়?

HTTP জন্ম থেকেই “stateless” — প্রতিটি request আলাদা, সার্ভার আগের request মনে রাখে না। শুধু দলিল দেখানোর ওয়েবে এতে সমস্যা ছিল না। কিন্তু ১৯৯৪ সালে Netscape Communications-এ Lou Montulli একটি সমস্যা পেলেন: একটি অনলাইন দোকানের অ্যাপ বানানো হচ্ছিল, আর ক্রেতা পাতা থেকে পাতায় গেলে তাঁর কার্ট কীভাবে মনে থাকবে? প্রতিটি ব্যবহারকারীর তথ্য সার্ভারে রাখা যায়, কিন্তু সার্ভার জানবে কীভাবে কোন request কোন ব্যবহারকারীর? URL-এ ID জুড়ে দেওয়া যেত, কিন্তু লিংক শেয়ার করলেই অন্যজন আপনার কার্ট পেতেন।

Montulli-র সমাধান ছিল সরল: সার্ভার ব্রাউজারকে একটি ছোট চিরকুট দেবে, ব্রাউজার সেটি রেখে দেবে আর একই সাইটে পরের প্রতিটি request-এ ফেরত দেবে। নামটি এসেছে প্রোগ্রামারদের পুরোনো শব্দ “magic cookie” থেকে — এক প্রোগ্রাম থেকে আরেক প্রোগ্রামে ফেরত যাওয়া একটি টোকেন। ১৯৯৪ সালের শেষ দিকে Netscape-এর ব্রাউজারে এটি এসে যায়, আর কোনো আনুষ্ঠানিক মানদণ্ড ছাড়াই অন্য ব্রাউজাররাও অনুসরণ করে।

তাড়াহুড়োর দাম আমরা আজও দিচ্ছি। প্রথমত, cookie origin মানে না — scheme বা port আলাদা হলেও একই host-এর cookie যায়; এই ফাঁক বন্ধ করতে বহু বছর ধরে নতুন নিয়ম (যেমন __Host- নামের prefix) যোগ করতে হয়েছে। দ্বিতীয়ত, cookie নিজে থেকে প্রতিটি request-এ যায় — অন্য সাইট থেকে শুরু হওয়া request-এও। এই “ambient authority” থেকেই জন্ম CSRF আক্রমণের, যা ঠেকাতে ২০১০-এর দশকের শেষে SameSite এল। তৃতীয়ত, প্রথম দিকে যেকোনো script cookie পড়তে পারত; Microsoft ২০০২ সালে Internet Explorer 6-এর একটি আপডেটে HttpOnly যোগ করে। আর document.cookie-এর অদ্ভুত string API সেই সময়েরই উত্তরাধিকার।

আনুষ্ঠানিক মানদণ্ড এসেছে অনেক পরে: RFC 2109 (১৯৯৭), RFC 2965 (২০০০) — যা বাস্তবে প্রায় কেউ মানেনি — আর শেষে RFC 6265 (২০১১), যা ব্রাউজারগুলো আসলে যা করে তা-ই লিখে রাখে। এর হালনাগাদ সংস্করণ (6265bis) এখনো তৈরির পথে।

মূল উপলব্ধি: একটি তাড়াহুড়োর সমাধান ব্যাপক হয়ে গেলে তাকে আর বদলানো যায় না, শুধু ওপরে পট্টি লাগানো যায়। Cookie-র প্রতিটি attribute — Secure, HttpOnly, SameSite — আসলে একেকটি পুরোনো নিরাপত্তা-সমস্যার পট্টি।

প্র ০২Web Storage আসার আগে ডেভেলপাররা কীভাবে ব্রাউজারে ডেটা রাখতেন? Flash-এর “supercookie” কী ছিল, আর HTML5-এর localStorage কোন সমস্যা মেটাল?

২০০০-এর দশকে ওয়েব অ্যাপ জটিল হচ্ছিল — Gmail (২০০৪), Google Maps (২০০৫) দেখাল ব্রাউজারেই আসল অ্যাপ চলতে পারে। কিন্তু ক্লায়েন্টে ডেটা রাখার একমাত্র মানসম্মত জায়গা ছিল cookie: প্রতিটি মাত্র প্রায় ৪ KB, আর প্রতিটি request-এ সার্ভারে যায়। ১০০ KB সেটিং cookie-তে রাখলে প্রতিটি ছবির request-ও সেই ভার বইত। ডেভেলপাররা তাই নানা কৌশল খুঁজলেন। Internet Explorer 5-এর নিজস্ব “userData” behavior ছিল, কিন্তু শুধু IE-তে।

সবচেয়ে জনপ্রিয় ছিল Adobe Flash-এর “Local Shared Objects” (LSO). তখন প্রায় সব কম্পিউটারে Flash plugin থাকত, আর একটি অদৃশ্য Flash ফাইল দিয়ে ডিফল্টে ১০০ KB পর্যন্ত ডেটা রাখা যেত — সব ব্রাউজারে একই জায়গায়। সমস্যা হলো, এই ডেটা ব্রাউজারের “cookie মুছুন” বোতামে মুছত না, কারণ এটি ছিল plugin-এর নিজের ফোল্ডারে। বিজ্ঞাপন আর ট্র্যাকিং কোম্পানিগুলো এর সুযোগ নিল: ব্যবহারকারী cookie মুছলেও Flash থেকে ID নিয়ে আবার cookie বসিয়ে দিত। এই কারণে এদের নাম হলো “supercookie” বা “zombie cookie”. ২০১০ সালে নিরাপত্তা-গবেষক Samy Kamkar “evercookie” নামে একটি প্রদর্শনী বানান, যা এক ডজনের বেশি জায়গায় একই ID লুকিয়ে দেখাত মুছে ফেলা কত কঠিন।

এর মধ্যেই HTML5-এর কাজের অংশ হিসেবে WHATWG-এর Ian Hickson-এর সম্পাদনায় Web Storage-এর নকশা হলো: সাধারণ key-value API, cookie-র চেয়ে অনেক বেশি জায়গা, সার্ভারে যায় না, আর ব্রাউজারের নিজের মোছার বোতামে মোছে। ২০০৯ থেকে ২০১০ সালের শুরুর মধ্যে Internet Explorer 8, Firefox 3.5, Safari 4 আর Chrome 4 — সবাই localStorage সমর্থন করল; W3C একে ২০১৩ সালে Recommendation করে। ২০১০ সালে Steve Jobs-এর “Thoughts on Flash” চিঠি আর iPhone-এ Flash না থাকা মিলে Flash-এর পতন শুরু হয়, আর ২০২০ সালের শেষে Adobe Flash-এর সমর্থন বন্ধ করে।

মূল উপলব্ধি: মানসম্মত পথ না থাকলে ডেভেলপাররা plugin আর কৌশলে চলে যান, আর সেই ফাঁক দিয়ে ব্যবহারকারীর নিয়ন্ত্রণ হারায়। Web Storage-এর বড় অর্জন শুধু বেশি জায়গা নয় — ডেটাকে ব্রাউজারের নিয়ম আর ব্যবহারকারীর “মুছুন” বোতামের আওতায় আনা।

প্র ০৩ব্রাউজারে একসময় আসল SQL database ছিল — WebSQL. সবাই SQL জানে, তবু কেন ২০১০ সালে এটি পরিত্যক্ত হলো আর তুলনায় জটিল IndexedDB জিতল?

২০০৯-১০ সালের দিকে Safari, Chrome আর Opera একটি API চালু করে, যার নাম Web SQL Database — পাতার JavaScript থেকে সরাসরি SELECT * FROM orders WHERE … চালানো যেত। ডেভেলপাররা খুশি, কারণ SQL সবাই জানে, আর iPhone-এর প্রথম দিকের অনেক অফলাইন ওয়েব অ্যাপ এর ওপর বানানো। কিন্তু ২০১০ সালের নভেম্বরে W3C এর মানদণ্ডের কাজ বন্ধ ঘোষণা করে। কেন?

সমস্যাটি ছিল মানদণ্ডের প্রক্রিয়া নিয়ে, প্রযুক্তি নিয়ে নয়। একটি ওয়েব মানদণ্ড হতে হলে অন্তত দুটি স্বাধীন বাস্তবায়ন লাগে — যাতে প্রমাণ হয় দলিল পড়েই যে কেউ জিনিসটি বানাতে পারে। কিন্তু WebSQL-এর সব বাস্তবায়ন ভেতরে একই SQLite library ব্যবহার করত। আর মানদণ্ডে “SQL” বলতে কোন SQL — সেটা লিখতে গেলে কার্যত লিখতে হতো “SQLite-এর নির্দিষ্ট সংস্করণ যা করে” — হাজারো ছোট আচরণ, এমনকি তার বাগসহ। একটি তৃতীয় পক্ষের library-র পুরো আচরণকে ওয়েবের চিরস্থায়ী মানদণ্ড বানানো, যা পরে কখনো বদলানো যাবে না — Mozilla আর Microsoft এতে রাজি হয়নি।

বিকল্প হিসেবে আসে Oracle-এর Nikunj Mehta-র প্রাথমিক প্রস্তাব থেকে জন্ম নেওয়া Indexed Database API — নিচু স্তরের একটি key-value store, index আর transaction-সহ। দর্শনটি ছিল: ব্রাউজার শুধু শক্ত, ছোট ভিত্তি দেবে; তার ওপর SQL-জাতীয় query, sync বা অন্য কিছু library বানাবে। Firefox 4 (২০১১) আর পরে Chrome, Internet Explorer 10 এটি আনে; W3C ২০১৫ সালে প্রথম সংস্করণকে Recommendation করে। দাম হলো জটিলতা — event-ভিত্তিক, লম্বা API, যার জন্য আজও idb বা Dexie.js-এর মতো মোড়ক লাগে।

WebSQL পুরোপুরি বিদায় নিতে অনেক সময় লেগেছে — Chrome ২০২৩ সালের শেষ দিকে ডিফল্টে এটি বন্ধ করে। মজার বিষয়, এখন SQLite-কে WebAssembly-তে compile করে ব্রাউজারে চালানো যায়, আর তার ডেটা রাখা যায় নতুন Origin Private File System-এ — অর্থাৎ SQL ফিরে এসেছে, তবে মানদণ্ড হিসেবে নয়, library হিসেবে।

মূল উপলব্ধি: প্ল্যাটফর্মের মানদণ্ডে “সুবিধাজনক” আর “টেকসই” এক জিনিস নয়। নিচু স্তরের সাধারণ ভিত্তি দিয়ে উঁচু স্তরের সুবিধা library-র হাতে ছেড়ে দেওয়া — এই নকশা-দর্শনকে বলে “extensible web”.

প্র ০৪Third-party cookie দিয়ে কীভাবে মানুষকে সাইট থেকে সাইটে অনুসরণ করা হতো, আর GDPR (২০১৮) ও Safari-র ITP (২০১৭) কীভাবে ওয়েবের এই অর্থনীতি বদলে দিল?

কৌশলটি সরল। একটি বিজ্ঞাপন-নেটওয়ার্ক হাজারো সাইটে তার বিজ্ঞাপন বা অদৃশ্য ১×১ পিক্সেলের ছবি বসায়। আপনি একটি খবরের সাইটে গেলেন — পিক্সেলের request গেল tracker.example-এ, সাথে একটি ID-ওয়ালা cookie বসল। তারপর একটি জুতার দোকানে গেলেন — সেখানেও একই tracker-এর পিক্সেল, আর ব্রাউজার একই cookie ফেরত পাঠাল, সাথে Referer বলে দিল আপনি কোন পাতায়। এভাবে একটি ID-র নিচে আপনার ভ্রমণের পুরো ইতিহাস জমে, আর তিন দিন পরে অন্য সাইটে সেই জুতার বিজ্ঞাপন হাজির হয় (“retargeting”). আপনি কখনো tracker-এর সাইটে যাননি, অথচ সে আপনাকে চেনে।

প্রথম বড় ধাক্কা আসে ব্রাউজার থেকে। ২০১৭ সালে Apple Safari 11-এ Intelligent Tracking Prevention চালু করে: যন্ত্র-শিক্ষা দিয়ে tracker চিহ্নিত করে তাদের cookie সীমিত করা। পরের কয়েক বছরে ITP ক্রমে কড়া হয় — script দিয়ে লেখা cookie-র মেয়াদ ৭ দিনে বাঁধা, ২০২০ সালে সব third-party cookie আটকানো, আর script-লেখা সব storage-এ ৭ দিনের সীমা। Firefox ২০১৯ সালে Enhanced Tracking Protection ডিফল্টে চালু করে, আর ২০২২ সালে Total Cookie Protection — প্রতিটি সাইটের জন্য আলাদা “cookie jar”, ফলে একই tracker দুই সাইটে দুটি আলাদা ID পায়।

দ্বিতীয় ধাক্কা আইনের। ইউরোপীয় ইউনিয়নের ২০০৯ সালের ePrivacy সংশোধনী (“cookie law”) ডিভাইসে তথ্য রাখার আগে সম্মতি চায়, আর ২০১৮ সালের মে মাসে কার্যকর হওয়া General Data Protection Regulation (GDPR) ব্যক্তিগত তথ্যের সংজ্ঞায় অনলাইন identifier-ও রাখে, সাথে বিশ্বব্যাপী আয়ের শতাংশ হিসাবে বিশাল জরিমানার বিধান। ফলাফল আমরা সবাই দেখি — প্রতিটি সাইটে cookie-সম্মতির banner. ক্যালিফোর্নিয়ার CCPA-সহ আরও অনেক জায়গায় একই ধাঁচের আইন এসেছে।

তবে ট্র্যাকিং থামেনি, রূপ বদলেছে: first-party ডেটা, লগইন-ভিত্তিক পরিচয়, আর browser fingerprinting — ফন্ট, স্ক্রিনের মাপ, GPU-র মতো খুঁটিনাটি মিলিয়ে cookie ছাড়াই ডিভাইস চেনা, যা মোছাও যায় না। আর Chrome-এর ঘুরপথের গল্প (অংশ ৫) দেখায়, বিজ্ঞাপন-নির্ভর একটি কোম্পানির পক্ষে এই সিদ্ধান্ত কতটা কঠিন।

মূল উপলব্ধি: একটি প্রযুক্তিগত সুবিধা (cookie নিজে থেকে যায়) একটি পুরো ব্যবসায়িক মডেলের ভিত্তি হয়ে উঠতে পারে, আর তা বদলাতে ব্রাউজার, আইন আর বাজার — তিনটির চাপই লাগে। ডেভেলপার হিসেবে আজ ধরে নিন: অন্য সাইটের context-এ আপনার storage সীমিত বা আলাদা থাকবে।

প্র ০৫“Offline-first” আর “local-first” সফটওয়্যার কী? ব্রাউজারের storage কেন শুধু cache নয়, বরং অ্যাপের মূল ডেটার জায়গা হয়ে উঠছে?

প্রচলিত ওয়েব অ্যাপের ধারণা হলো: সত্যিকারের ডেটা থাকে সার্ভারে, ব্রাউজার শুধু দেখায়। নেটওয়ার্ক না থাকলে অ্যাপ অচল — বাংলাদেশে লোডশেডিংয়ে রাউটার বন্ধ, বা ট্রেনে মোবাইল নেটওয়ার্ক হারানো ব্যবহারকারী এটা ভালো জানেন। ২০১০-এর দশকের মাঝামাঝি “offline-first” আন্দোলন বলল: ধরে নিন নেটওয়ার্ক থাকবে না, আর থাকলে সেটি বাড়তি সুবিধা। অ্যাপ আগে local storage-এ (IndexedDB) লেখে, ব্যবহারকারী সঙ্গে সঙ্গে ফল দেখেন, আর পটভূমিতে সময় পেলে সার্ভারের সাথে মেলায় — এই পর্বের outbox ঠিক এই নকশা। Service worker আর Cache API (পর্ব ৪৪) এর বাকি অংশ।

২০১৯ সালে গবেষণা-প্রতিষ্ঠান Ink & Switch-এর Martin Kleppmann, Adam Wiggins, Peter van Hardenberg আর Mark McGranaghan “Local-first software: You own your data, in spite of the cloud” নামে একটি প্রভাবশালী প্রবন্ধ লেখেন। তাঁদের যুক্তি আরও গভীর: শুধু অফলাইনে চলা নয়, ডেটার মূল কপি থাকবে ব্যবহারকারীর ডিভাইসে, সার্ভার হবে শুধু sync আর backup-এর সহায়ক। তাঁরা সাতটি আদর্শ রাখেন — দ্রুত (নেটওয়ার্কের অপেক্ষা নেই), একাধিক ডিভাইসে চলে, অফলাইনে চলে, একসাথে কয়েকজন কাজ করতে পারে, কোম্পানি বন্ধ হলেও ডেটা টেকে, নিরাপত্তা ও গোপনীয়তা, আর ব্যবহারকারীর পূর্ণ মালিকানা।

কঠিন অংশ হলো মেলানো: আপনি অফলাইনে একটি লেখা বদলালেন, সহকর্মী একই সময়ে অন্য জায়গায় বদলালেন — কোনটি থাকবে? এর জন্য গবেষকেরা CRDT (Conflict-free Replicated Data Type) নামে বিশেষ ডেটা-কাঠামো ব্যবহার করেন, যা যে ক্রমেই বদল আসুক, শেষে সব কপিকে একই অবস্থায় পৌঁছে দেয়। Automerge আর Yjs-এর মতো JavaScript library এই কাজ করে, আর তাদের ডেটা রাখে IndexedDB-তে।

এই দর্শনে ব্রাউজারের storage-এর দুর্বলতাগুলো বড় প্রশ্ন হয়ে দাঁড়ায়: quota ফুরালে ব্রাউজার নিজে মুছতে পারে, Safari ৭ দিনে মুছতে পারে, ব্যবহারকারী এক ক্লিকে সব মুছে ফেলতে পারেন। তাই local-first অ্যাপ navigator.storage.persist() চায়, ইনস্টল করা PWA হতে চায়, আর এক বা একাধিক জায়গায় sync রাখে।

মূল উপলব্ধি: ব্রাউজারের storage-কে “cache” হিসেবে ভাবলে আপনি সার্ভার-কেন্দ্রিক অ্যাপ বানান; “database” হিসেবে ভাবলে দ্রুত, অফলাইনে চলা অ্যাপ বানাতে পারেন — কিন্তু তখন মেয়াদ, quota, migration আর মেলানোর দায়িত্বও আপনার।

অনুশীলন

এই কাজগুলো যেকোনো সাধারণ ওয়েবসাইট খুলে DevTools console-এ (F12) করুন — এই পাতার কোড-বাক্সে storage বন্ধ। ফল আগে অনুমান করুন, তারপর মিলিয়ে নিন।

  1. ফল অনুমান করুন: পরপর চালালে প্রতিটি লাইন কী দেবে? (ক) localStorage.setItem("n", 5); typeof localStorage.getItem("n") (খ) localStorage.setItem("a", [1, 2]); localStorage.getItem("a") (গ) localStorage.getItem("নেই") (ঘ) JSON.parse(localStorage.getItem("নেই")) (ঙ) localStorage.setItem("x", undefined); JSON.parse(localStorage.getItem("x"))

    (ক) "string" — 5 রাখা হয়েছে "5" হয়ে। (খ) "1,2" — array-র toString(), JSON নয়; ফেরত parse করলে "1,2" বৈধ JSON নয়। (গ) null — key নেই। (ঘ) null — JSON.parse(null) আগে null-কে "null" লেখা বানায়, তাই error নয়। (ঙ) SyntaxError — রাখা হয়েছে string "undefined", যা বৈধ JSON নয়; Edge/Chrome বলে "undefined" is not valid JSON. শিক্ষা: storage-এর প্রতিটি parse try/catch-এ, আর রাখার আগে সবসময় JSON.stringify.

  2. কোন তাক কোন কাজে: প্রতিটির জন্য localStorage, sessionStorage, cookie বা IndexedDB বাছুন আর এক লাইনে কারণ বলুন: (ক) ব্যাংক অ্যাপের লগইন session (খ) ভাষা বাছাই “বাংলা/English” (গ) অনলাইন পরীক্ষার একটি ট্যাবে বর্তমান প্রশ্ন নম্বর (ঘ) অফলাইনে ব্যবহারের জন্য ৫,০০০ পণ্যের ছবিসহ তালিকা (ঙ) সার্ভারকে প্রতিটি পাতায় জানাতে হবে ব্যবহারকারী কোন ভাষা চান।

    (ক) Cookie — HttpOnly; Secure; SameSite=Strict বা Lax, যাতে script পড়তে না পারে। (খ) localStorage — ছোট, স্থায়ী, সার্ভারের দরকার নেই। (গ) sessionStorage — শুধু এই ট্যাবের, রিলোডে টেকে, আর দুই ট্যাবে দুই পরীক্ষা গুলিয়ে যায় না। (ঘ) IndexedDB — বড় ডেটা, Blob, index দিয়ে খোঁজা; localStorage-এর ৫ MB-এ ধরবে না। (ঙ) Cookie — সার্ভার প্রথম request-এই ভাষা জেনে পাতা সেই ভাষায় পাঠায়। মূল কথা: শুধু ব্রাউজারের দরকার হলে Web Storage, সার্ভারের দরকার হলে cookie.

  3. DevTools-এ পর্যবেক্ষণ: একই সাইট দুটি ট্যাবে খুলুন। দুটির console-এ লিখুন addEventListener("storage", e => console.log(e.key, e.oldValue, e.newValue)). তারপর প্রথম ট্যাবে localStorage.setItem("demo", "1"), আবার একই লাইন, তারপর "2" দিয়ে। কোন ট্যাবে কী ছাপা হলো? এরপর Application ট্যাবে Cookies অংশ খুলে দেখুন কোন cookie-তে HttpOnly টিক আছে, আর console-এর document.cookie-এ সেগুলো আছে কি না।

    প্রথম ট্যাবে কিছুই ছাপা হয় না — লেখক নিজে event পায় না। দ্বিতীয় ট্যাবে প্রথমবার demo null 1; একই মান আবার লেখায় কিছু নয় (মান বদলায়নি); তারপর demo 1 2. Cookie-র অংশে প্রায় যেকোনো লগইন-করা সাইটে অন্তত একটি HttpOnly cookie পাবেন (সাধারণত session-এর নাম), আর document.cookie-এ সেটি থাকবে না — DevTools সব দেখায়, কারণ এটি ব্যবহারকারীর নিজের টুল, পাতার script নয়।

  4. AI অডিট (কোড পড়া ও সংশোধন): একটি AI এই লগইন কোড দিল: localStorage.setItem("user", JSON.stringify({ email, password, token })), আর প্রতিটি পাতায় const user = JSON.parse(localStorage.getItem("user")); if (user.isAdmin) showAdminPanel();. অন্তত চারটি সমস্যা খুঁজুন আর প্রতিটির সমাধান লিখুন।

    (১) পাসওয়ার্ড ব্রাউজারে রাখা — কোথাও নয়; লগইনের পর পাসওয়ার্ডের কোনো প্রয়োজনই নেই। (২) token localStorage-এ — XSS হলে চুরি; সার্ভার HttpOnly, Secure, SameSite cookie-তে session রাখুক। (৩) JSON.parse try/catch ছাড়া, আর লগআউট অবস্থায় user হবে null — তখন user.isAdmin দেয় TypeError: Cannot read properties of null; সমাধান try/catch আর user?.isAdmin. (৪) সবচেয়ে গুরুতর: isAdmin ব্রাউজারের storage থেকে বিশ্বাস করা — যে কেউ DevTools-এ "isAdmin": true লিখে admin প্যানেল খুলতে পারে। প্যানেল লুকানো শুধু UI-এর সুবিধা; প্রতিটি admin কাজ সার্ভারে session দেখে অনুমতি যাচাই করবে। (৫) বাড়তি: সংস্করণ বা মেয়াদ নেই, আর শেয়ার করা কম্পিউটারে ইমেইলও খোলা পড়ে থাকে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি কার্ট object localStorage-এ রেখে রিলোডের পর নিরাপদে ফেরত আনার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. কার্ট object-কে সংস্করণ আর মেয়াদসহ একটি খামে রাখা
  2. JSON.stringify দিয়ে খামটিকে string বানানো
  3. try/catch-এর ভেতরে setItem দিয়ে রাখা
  4. রিলোডের পর getItem দিয়ে string পড়া
  5. try/catch-এর ভেতরে JSON.parse, তারপর সংস্করণ ও মেয়াদ যাচাই

localStorage.setItem("cart", { id: 7 }) চালানোর পর localStorage.getItem("cart") কী দেয়?

  • { id: 7 } object
  • "[object Object]"
  • '{"id":7}'
  • TypeError

একটি সাইট দুটি ট্যাবে খোলা। প্রথম ট্যাবের কোড localStorage.setItem("theme", "dark") দিয়ে আগের "light" মান বদলাল। storage event কোথায় fire হবে?

  • শুধু প্রথম ট্যাবে
  • দুই ট্যাবেই
  • শুধু দ্বিতীয় ট্যাবে
  • কোথাও না

লগইন session-এর cookie-তে HttpOnly attribute দেওয়ার মূল উদ্দেশ্য কী?

  • পাতার JavaScript যেন cookie পড়তে না পারে, যাতে XSS হলেও session ID চুরি না হয়
  • cookie যেন শুধু HTTPS-এ পাঠানো হয়
  • অন্য সাইট থেকে আসা request-এ cookie না পাঠানো
  • ব্রাউজার বন্ধ হলেই cookie মুছে ফেলা

sessionStorage সম্পর্কে কোন কথাটি সঠিক?

  • একই origin-এর সব ট্যাব একই sessionStorage ভাগ করে
  • পাতা রিলোড করলেই মুছে যায়
  • প্রতিটি request-এর সাথে সার্ভারে যায়
  • রিলোডে টিকে থাকে, কিন্তু ট্যাব বন্ধ করলে মুছে যায়

একটি অফলাইন অ্যাপে কয়েক হাজার পণ্য, ছবিসহ, ডিভাইসে রেখে index দিয়ে খুঁজতে হবে। কোন storage সবচেয়ে উপযুক্ত?

  • localStorage
  • IndexedDB
  • Cookie
  • sessionStorage

https://shop.example.com-এর localStorage নিচের কোন ঠিকানা থেকে পড়া যায়?

  • http://shop.example.com
  • https://www.example.com
  • https://shop.example.com/cart?id=7
  • https://shop.example.com:8443

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. ব্রাউজারের storage origin ধরে আলাদা, কিন্তু নিজের origin-এর যেকোনো script দেখে আর ব্যবহারকারী মুছতে পারেন — তাই গোপন তথ্য নয়, অন্ধ বিশ্বাসও নয়.
  2. localStorage স্থায়ী, sessionStorage ট্যাবের; দুটোই শুধু string, synchronous, কয়েক MB — JSON.stringify দিয়ে রাখুন, try/catch-এ parse করুন।
  3. প্রথম হাতে দেওয়াও try/catch-এ — storage বন্ধ বা sandbox-এ SecurityError; ডেটায় সংস্করণ আর মেয়াদ রাখুন।
  4. storage event যায় অন্য ট্যাবে; cookie যায় প্রতিটি request-এ — লগইনের জন্য HttpOnly; Secure; SameSite.
  5. বড়, কাঠামোবদ্ধ বা অফলাইন ডেটায় IndexedDB — asynchronous, transaction, structured clone; transaction-এর মাঝে নেটওয়ার্কের অপেক্ষা নয়।

পরের পর্বে: ব্রাউজার শুধু ডেটা রাখে না — কপি করা লেখা পড়তে পারে, অবস্থান বলতে পারে, notification দেখাতে পারে, আর কোন element পর্দায় এলো তা জানাতে পারে। Clipboard, Geolocation, Notification আর Observer API — আর কেন এদের বেশিরভাগ আগে ব্যবহারকারীর অনুমতি চায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান