পাঠ ৩৬ · ৫৭-এর মধ্যে · মডিউল ৮
Home / Courses / Mobile App Development / লোকাল স্টোরেজ

অফলাইন-ফার্স্ট ডেটা স্ট্র্যাটেজি

Offline-first data strategy
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • অফলাইন-ফার্স্ট ডিজাইনের মূল নীতি — লোকাল-প্রথম রাইট, লোকাল-প্রথম রিড
  • একটি পেন্ডিং সিঙ্ক কিউ কীভাবে লোকাল রাইট আর রিমোট সিঙ্কের মধ্যে ডিকাপলিং তৈরি করে
  • sync()-এর আগে ও পরে লোকাল ডেটার অবস্থা তুলনা করে বোঝা কেন এটি "সবসময় কাজ করে"
  • রিট্রাই/ব্যাকঅফ দিয়ে একটি সিমুলেটেড ব্যর্থ নেটওয়ার্ক কল সামলে সিঙ্ক সম্পন্ন করা

১ · লোকাল-প্রথম রাইট, লোকাল-প্রথম রিড

L01-এই আমরা দেখেছি একটি মোবাইল অ্যাপ ইন্টারনেট হারাতে পারে যেকোনো মুহূর্তে — সাবওয়ে, দুর্বল সিগন্যাল, এয়ারপ্লেন মোড। একটি ওয়েব পেজের মতো "সার্ভার রেসপন্স না আসা পর্যন্ত অপেক্ষা করা" মডেল মোবাইলে খারাপ অভিজ্ঞতা দেয়। অফলাইন-ফার্স্টOffline-Firstএকটি ডিজাইন কৌশল যেখানে প্রতিটি রাইট প্রথমে লোকাল স্টোরেজে সংরক্ষিত হয় (নেটওয়ার্কের উপর নির্ভর না করে), আর নেটওয়ার্ক সিঙ্ক একটি আলাদা, পরে-ঘটা ধাপ। কৌশলে এর সমাধান উল্টো: রাইট আগে লোকাল স্টোরেজে যায় (তাৎক্ষণিক সফল), তারপর একটি কিউতে যোগ হয় পরে সিঙ্ক হওয়ার জন্য। রিড কখনোই সরাসরি নেটওয়ার্কের উপর নির্ভর করে না — সবসময় লোকাল স্টোরেজ থেকে আসে, যা সিঙ্ক দ্বারা সময়ে সময়ে আপডেট হতে থাকে।

লোকাল রাইট
তাৎক্ষণিক, সবসময় সফল — নেটওয়ার্কের উপর নির্ভর করে না।
পেন্ডিং সিঙ্ক কিউ
প্রতিটি লোকাল রাইট একটি অপারেশন হিসেবে জমা হয়, রিমোটে পাঠানোর অপেক্ষায়।
sync()
নেটওয়ার্ক থাকলে কিউ প্রসেস করে, রিমোট আপডেট করে, সফল এন্ট্রি কিউ থেকে সরায়।

২ · লোকাল রাইট, পেন্ডিং কিউ, আর sync()

নিচের কোড সেলে প্রথমে দুটো নোট লোকালি তৈরি করা হচ্ছে এবং একটি আপডেট করা হচ্ছে — sync() এখনো কল হয়নি, তবু লোকাল ডেটা সাথে সাথে সঠিক ও ব্যবহারযোগ্য। তারপর একটি সিমুলেটেড রিমোট সার্ভার আর একটি ডিটারমিনিস্টিক ফ্লেকি নেটওয়ার্ক ফাংশন সংজ্ঞায়িত করে sync() কল করা হচ্ছে — L38-এ পূর্ণাঙ্গভাবে শেখা এক্সপোনেনশিয়াল ব্যাকঅফ ধারণাটি ব্যবহার করে (delay = min(base * 2**attempt, max_delay))।

Python
# ---------- লোকাল স্টোরেজ + পেন্ডিং সিঙ্ক কিউ ----------
local_notes = {}          # লোকাল "ডেটাবেস" -- key: note_id
pending_sync_queue = []   # প্রতিটি এন্ট্রি: {"op":.., "note_id":.., "data":..}
_next_id = 1

def create_note_locally(title, body):
    global _next_id
    note_id = _next_id
    _next_id += 1
    note = {"id": note_id, "title": title, "body": body, "synced": False}
    local_notes[note_id] = note                                     # লোকাল স্টেট সাথে সাথে আপডেট
    pending_sync_queue.append({"op": "create", "note_id": note_id, "data": dict(note)})
    print(f"[লোকাল] নোট #{note_id} তৈরি হলো: {note}")
    return note_id

def update_note_locally(note_id, **changes):
    local_notes[note_id].update(changes)                            # লোকাল স্টেট সাথে সাথে আপডেট
    pending_sync_queue.append({"op": "update", "note_id": note_id, "data": dict(changes)})
    print(f"[লোকাল] নোট #{note_id} আপডেট হলো: {local_notes[note_id]}")

# ---------- ইউজার অফলাইনে থাকলেও এই তিনটি রাইট তাৎক্ষণিক সফল হয় ----------
create_note_locally("Grocery list", "Milk, eggs")
create_note_locally("Trip plan", "Flights, hotel")
update_note_locally(1, pinned=True)

print(f"\nsync() কল করার আগেই লোকাল ডেটা সম্পূর্ণ ব্যবহারযোগ্য:")
print(f"  local_notes = {local_notes}")
print(f"  পেন্ডিং সিঙ্ক কিউতে অপেক্ষমান: {len(pending_sync_queue)}টি অপারেশন "
      f"{[e['op']+'#'+str(e['note_id']) for e in pending_sync_queue]}")

    
লক্ষ্য করুন sync() এখনো একবারও কল হয়নি, তবু local_notes-এ দুটো নোট পুরোপুরি সঠিক ডেটাসহ আছে (id ১-এর pinned-ও ইতিমধ্যে True) — একজন ইউজার এই মুহূর্তে অ্যাপের UI-তে এই ডেটা দেখলে কোনো পার্থক্য টের পাবেন না, ইন্টারনেট থাকুক বা না থাকুক। শুধু pending_sync_queue-এ তিনটি এন্ট্রি জমা হয়ে আছে, রিমোট সার্ভারে এখনো কিছুই পাঠানো হয়নি — এটাই অফলাইন-ফার্স্টের মূল চুক্তি: রাইট আর সিঙ্ক সম্পূর্ণ আলাদা ধাপ।
Python
# ---------- সিমুলেটেড রিমোট সার্ভার + ডিটারমিনিস্টিক ফ্লেকি নেটওয়ার্ক ----------
remote_notes = {}
FAIL_PLAN = {2: 2}        # note_id 2 প্রথম ২ বার ব্যর্থ হবে, ৩য় চেষ্টায় সফল -- ডিটারমিনিস্টিক
_attempts_made = {}

def _send_to_remote(entry):
    note_id = entry["note_id"]
    must_fail = FAIL_PLAN.get(note_id, 0)
    _attempts_made[note_id] = _attempts_made.get(note_id, 0) + 1
    attempt = _attempts_made[note_id]
    if attempt <= must_fail:
        return False
    if entry["op"] == "create":
        remote_notes[note_id] = dict(entry["data"])
    elif entry["op"] == "update":
        remote_notes.setdefault(note_id, {}).update(entry["data"])
    return True

def sync(max_attempts=4, base_delay=0.5, max_delay=4.0):
    print("--- sync() শুরু ---")
    remaining = []
    for entry in pending_sync_queue:
        op_label = f"{entry['op']}#{entry['note_id']}"
        synced = False
        for attempt in range(max_attempts):
            ok = _send_to_remote(entry)
            if ok:
                print(f"  {op_label}: চেষ্টা {attempt + 1} -- সফল")
                synced = True
                break
            delay = min(base_delay * (2 ** attempt), max_delay)
            print(f"  {op_label}: চেষ্টা {attempt + 1} -- ব্যর্থ, {delay:.2f}s ব্যাকঅফের পর আবার চেষ্টা")
        if synced:
            local_notes[entry["note_id"]]["synced"] = True
        else:
            print(f"  {op_label}: সর্বোচ্চ {max_attempts} চেষ্টার পরও ব্যর্থ -- কিউতেই থেকে যাবে")
            remaining.append(entry)
    pending_sync_queue[:] = remaining
    print(f"--- sync() শেষ -- বাকি পেন্ডিং এন্ট্রি: {len(pending_sync_queue)}টি ---")

sync()

print(f"\nremote_notes = {remote_notes}")
print(f"local_notes  = {local_notes}")
print(f"পেন্ডিং সিঙ্ক কিউ (sync()-এর পর): {pending_sync_queue}")

    
এই দ্বিতীয় কোড সেলটি প্রথমটির উপর নির্ভর করে (একই পেজে প্রথমটি রান করার পরের অবস্থা ব্যবহার করে)। FAIL_PLAN = {'{'}2: 2{'}'} মানে note_id=2-এর অপারেশনটি (যার op="create") প্রথম দুইবার ব্যর্থ হবে, তিন নম্বর চেষ্টায় সফল হবে — note_id=1-এর দুটো অপারেশন (create ও update) প্রতিবারই প্রথম চেষ্টাতেই সফল হয়, কারণ FAIL_PLAN.get(1, 0) শূন্য। তাই create#2-এর জন্য ব্যাকঅফ ডিলে দুইবার প্রিন্ট হয় (০.৫০s, তারপর ১.০০s — 0.5 * 2**0 ও 0.5 * 2**1), তারপর তিন নম্বর চেষ্টায় সফল হয়। তিনটি অপারেশনই শেষ পর্যন্ত সফল হওয়ায় pending_sync_queue সম্পূর্ণ খালি হয়ে যায়, আর remote_notes-এ এখন লোকাল ডেটার সাথে মেলে এমন দুটো নোট দেখা যায়।
মূল কথা · Key takeaway

অফলাইন-ফার্স্ট ডিজাইনে "সবসময় কাজ করা" আর "সার্ভারের সাথে মেলানো" — এই দুটো আলাদা চিন্তা। লোকাল রাইট প্রথম চিন্তাটি সাথে সাথেই সমাধান করে দেয়; পেন্ডিং সিঙ্ক কিউ আর sync() দ্বিতীয় চিন্তাটি নেটওয়ার্ক ফিরে এলে পরে সমাধান করে, প্রয়োজনে রিট্রাই করে। এই ডিকাপলিংই মোবাইল অ্যাপকে অস্থির নেটওয়ার্কে নির্ভরযোগ্য মনে করায় — L38-এ আমরা রিট্রাই/ব্যাকঅফ আর টাইমআউট আরও গভীরভাবে দেখব।

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

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ create_note_locally ফাংশনে local_notes[note_id] = note লাইনটি pending_sync_queue.append(...)-এর আগে কেন লেখা হয়েছে?

কারণ অফলাইন-ফার্স্টের মূল প্রতিশ্রুতি হলো লোকাল স্টেট সাথে সাথেই সঠিক হওয়া উচিত — কিউতে অপারেশন যোগ করা শুধু "পরে সিঙ্ক করতে হবে" এই তথ্যটুকু রেকর্ড করে, লোকাল রাইটের সফলতার উপর নির্ভর করে না। ক্রম উল্টো হলেও কার্যকরীভাবে একই ফল হতো এই নির্দিষ্ট উদাহরণে, তবে লজিক্যালি লোকাল স্টেট আপডেটকেই প্রাইমারি সোর্স অফ ট্রুথ হিসেবে আগে বসানো এই প্যাটার্নের উদ্দেশ্যকে স্পষ্ট করে।

প্র ০২ sync()-এ যদি create#2 চার নম্বর চেষ্টাতেও ব্যর্থ হতো (অর্থাৎ FAIL_PLAN-এ {'{'}2: 5{'}'} থাকত), তাহলে pending_sync_queue-এর চূড়ান্ত অবস্থা কী হতো?

max_attempts=4-এর ভেতর create#2 কখনো সফল না হওয়ায় সেই এন্ট্রিটি remaining তালিকায় যোগ হতো এবং শেষে pending_sync_queue[:] = remaining-এর মাধ্যমে কিউতেই থেকে যেত — অন্য দুটো এন্ট্রি (যেগুলো সফল হয়েছে) কিউ থেকে সরে যেত। অর্থাৎ কিউ সম্পূর্ণ খালি হতো না, শুধু ব্যর্থ অপারেশনটি বাকি থাকত, পরের কোনো sync() কলে আবার চেষ্টা করার জন্য।

প্র ০৩ এই সিমুলেশনে ইউজার যদি sync() কল হওয়ার আগেই অ্যাপ বন্ধ করে দিতেন, তাহলে তৈরি করা দুটো নোট কি হারিয়ে যেত?

এই Pyodide সিমুলেশনে হ্যাঁ, কারণ local_notes শুধু RAM-এ থাকা একটি dict। কিন্তু একটি বাস্তব ডিভাইসে create_note_locally-এর ভেতরের লোকাল রাইটটি L33-এর মতো একটি ডিস্ক-ব্যাকড লোকাল ডেটাবেসে (বা L32-এর কী-ভ্যালু স্টোরে) হতো — তাই অ্যাপ বন্ধ হলেও নোট দুটো এবং pending_sync_queue-ও ডিস্কে টিকে থাকত, আর অ্যাপ পরের বার চালু হলে সেই অসম্পূর্ণ কিউ থেকেই sync() আবার চেষ্টা চালিয়ে যেত।

অনুশীলন

  1. চিন্তা করুন: একটি চ্যাট অ্যাপে মেসেজ পাঠানোর বাটনে চাপার সাথে সাথেই মেসেজটি স্ক্রিনে দেখা যায় (একটি ছোট "পাঠানো হচ্ছে..." আইকনসহ), নেটওয়ার্ক রেসপন্সের জন্য অপেক্ষা না করেই — এটি এই পাঠের কোন ধারণার সাথে মেলে?

    এটি ঠিক অফলাইন-ফার্স্ট লোকাল-প্রথম রাইট প্যাটার্ন — মেসেজটি সাথে সাথে লোকাল স্টেটে (আর UI-তে) যোগ হয়ে যায়, আর একটি পেন্ডিং সিঙ্ক এন্ট্রি তৈরি হয়। "পাঠানো হচ্ছে..." আইকনটি সেই এন্ট্রির synced: False অবস্থা প্রতিফলিত করে; নেটওয়ার্ক সিঙ্ক সফল হলে আইকনটি একটি "✓ পৌঁছেছে" চিহ্নে বদলে যায় — ঠিক আমাদের কোড সেলে synced ফ্ল্যাগ True হওয়ার মতোই।

  2. পরীক্ষা করুন: দ্বিতীয় কোড সেলে FAIL_PLAN = {'{'}2: 2{'}'}-কে FAIL_PLAN = {'{'}1: 1, 2: 1{'}'}-এ বদলে দিন (উভয় নোট এখন একবার করে ব্যর্থ হবে), তারপর রান করে দেখুন sync()-এর আউটপুটে কয়টি "ব্যর্থ" বার্তা দেখা যায় এবং শেষে কিউ খালি হয় কি না।

    তিনটি পেন্ডিং অপারেশনের মধ্যে create#1 ও create#2 প্রতিটি একবার করে ব্যর্থ হয়ে (মোট ২টি "ব্যর্থ" বার্তা) দ্বিতীয় চেষ্টায় সফল হবে, আর update#1 (যেহেতু FAIL_PLAN-এ note_id ধরে ট্র্যাক হয়, আর নোট ১-এর জন্য _attempts_made[1] ততক্ষণে ইতিমধ্যে ১ হয়ে গেছে create#1-এর কারণে) প্রথম চেষ্টাতেই সফল হবে, কারণ attempt তখন ২, যা must_fail=1-এর চেয়ে বড়। সব মিলিয়ে max_attempts=4-এর মধ্যেই সবগুলো সফল হয়ে কিউ পুরোপুরি খালি হয়ে যাবে।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অ্যাপ লাইফসাইকেল, মোবাইল UI/UX, MVVM/MVI আর্কিটেকচার, নেভিগেশন, স্টেট ম্যানেজমেন্ট, লোকাল স্টোরেজ, ডিভাইস ফিচার ও ডিপ্লয়মেন্ট — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • পরের পাঠ L37 মোবাইলে নেটওয়ার্ক রিকোয়েস্ট করা — ওয়েব থেকে পার্থক্য — নেটওয়ার্কিং মডিউলের শুরু।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics, Full-Stack Web Frameworks ও Mobile App Development — সব এক জায়গায়।
আগের পাঠ
মোবাইলে ডেটা মাইগ্রেশন