২০ আগ, ২০২৫·7 মিনিট পড়তে

Kotlin + SQLite-এর জন্য অফলাইন-প্রথম ফর্ম সংঘাত সমাধান

অফলাইন-প্রথম ফর্ম সংঘাত সমাধান শিখুন: স্পষ্ট মার্জ নিয়ম, সহজ Kotlin + SQLite সিঙ্ক ফ্লো, এবং এডিট সংঘাতের জন্য ব্যবহারিক UX প্যাটার্ন।

Kotlin + SQLite-এর জন্য অফলাইন-প্রথম ফর্ম সংঘাত সমাধান

দুই জন যখন অফলাইনে একসাথে এডিট করে তখন আসলে কি ঘটে

অফলাইন-প্রথম ফর্মগুলো মানুষকে নেটওয়ার্ক ধীর বা অনুপলব্ধ থাকলেও ডেটা দেখার এবং সম্পাদনার সুযোগ দেয়। সার্ভারের জন্য অপেক্ষা করার বদলে অ্যাপ প্রথমে লোকাল SQLite ডাটাবেসে পরিবর্তনগুলো লিখে, তারপর পরে সিঙ্ক করে।

এটি অনুভবে তৎক্ষণাত, কিন্তু একটি সরল বাস্তবতা তৈরি করে: দুই ডিভাইস একই রেকর্ড পরিবর্তন করতে পারে তাদের একে অপরের সম্পর্কে জানাই না।

একটি সাধারণ সংঘাত এরকম: একজন ফিল্ড টেকনিশিয়ান একটি ট্যাবলেটে ব্লুটুথ/সিগন্যাল ছাড়া একটি বেসমেন্টে একটি ওয়ার্ক অর্ডার খুলে রাখে। তারা status কে "Done" করে চিহ্নিত করে এবং একটি নোট যোগ করে। একই সময়ে, একজন সুপারভাইজার অন্য একটি ফোনে একই ওয়ার্ক অর্ডার আপডেট করে, কাউকে পুনরায় নির্ধারণ করে এবং due date সম্পাদনা করে। দুজনেই Save চাপে। দুজনেরই লোকাল সেভ সফল। এখানে কেউও ভুল করেনি।

যখন সিঙ্ক শেষমেষ হয়, সার্ভারকে সিদ্ধান্ত নিতে হয় কোন রেকর্ডটি "বাস্তব"। যদি আপনি সংঘাতগুলো স্পষ্টভাবে হ্যান্ডল না করেন, সাধারণত আপনি এইগুলোর মধ্যে একটি ফলাফল পাবেন:

  • Last write wins: পরে সিঙ্ক হওয়া পরিবর্তনগুলো আগের পরিবর্তনগুলোকে ওভাররাইট করে এবং কেউ ডেটা হারায়।
  • কঠোর ব্যর্থতা: সিঙ্ক একটি আপডেট প্রত্যাখ্যান করে এবং অ্যাপ একটি অপ্রয়োজনীয় ত্রুটি দেখায়।
  • ডুপ্লিকেট রেকর্ড: সিস্টেম ওভাররাইট এড়াতে দ্বিতীয় কপি তৈরি করে এবং রিপোর্ট জটিল হয়।
  • নীরব মার্জ: সিস্টেম পরিবর্তনগুলো একত্র করে, কিন্তু ক্ষেত্রগুলো এমনভাবে মিক্স করে যা ব্যবহারকারীরা প্রত্যাশা করে না।

সংঘাতগুলো বাগ নয়। এগুলো হল অফলাইন-প্রথম কাজ করার স্বাভাবিক ফলাফল — যেটাই এই প্যাটার্নের উদ্দেশ্য।

লক্ষ্যটি দু-মুখী: ডেটা রক্ষা করা এবং অ্যাপ সহজ রাখা। সাধারণত এর মানে স্পষ্ট মার্জ নিয়ম (প্রায়ই ফিল্ড-লেভেলে) এবং একটি ব্যবহারকারি অভিজ্ঞতা যা কেবল তখনই ব্যবহারকারীকে বিঘ্নিত করে যখন সত্যিই প্রয়োজন। যদি দুই এডিট ভিন্ন ফিল্ডে দেওয়া হয়, আপনি সাধারণত নীরবে মার্জ করতে পারবেন। যদি দুইজনই একই ফিল্ড ভিন্নভাবে পরিবর্তন করে, অ্যাপ সেটি দৃশ্যমান করে উচিৎ এবং কাউকে সঠিক ফলাফল বেছে নিতে সাহায্য করা উচিৎ।

আপনার ডেটার সাথে খাপ খাওয়ানো একটি সংঘাত কৌশল বেছে নিন

সংঘাতগুলো প্রথমে একটি প্রযুক্তিগত সমস্যা নয়। এগুলো হল পণ্যের সিদ্ধান্ত — যখন দুইজন একই রেকর্ড সিঙ্কের আগে পরিবর্তন করেছে তখন "সঠিক" মান কী তা নির্ধারণ করা।

তিনটি কৌশল অধিকাংশ অফলাইন অ্যাপ কভার করে:

  • Last write wins (LWW): নতুনতম এডিট গ্রহণ করে পুরোনোটি ওভাররাইট করুন।
  • Manual review: থেমে একজন মানুষকে কি রাখা হবে তা বেছে করাতে বলুন।
  • Field-level merge: প্রতিটি ফিল্ডভিত্তিকভাবে পরিবর্তনগুলো মিলিয়ে নিন এবং কেবল সেই ক্ষেত্রগুলোতে জিজ্ঞাসা করুন যেখানে দুইজনই একই ফিল্ড স্পর্শ করেছে।

LWW ঠিক থাকতে পারে যখন স্পিড পারফেক্ট একিউরেসির চাইতে বেশি গুরুত্বপূর্ণ এবং ভুলের খরচ কম। ভাবুন: অভ্যন্তরীণ নোট, অ-সক্রিয় ট্যাগ, বা ড্রাফট স্ট্যাটাস যেটা পরে আবার এডিট করা যাবে।

ম্যানুয়াল রিভিউ বেশি নিরাপদ যখন উচ্চ-প্রভাব ফিল্ড থাকে যেখানে অ্যাপ অনুমান করা উচিত নয়: আইনি পাঠ্য, কমপ্লায়েন্স কনফার্মেশন, পেয়রোল এবং ইনভয়েসিং পরিমাণ, ব্যাংকিং বিশদ, ওষুধ নির্দেশনা — এবং যেকোনো কিছু যা দায় সৃষ্টি করতে পারে।

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

কিছু করতে যাওয়ার আগে লিখে রাখুন আপনার ব্যবসার জন্য "সঠিক" কী মানে। একটি দ্রুত চেকলিস্ট সহায়ক:

  • কোন কোন ফিল্ড অবশ্যই সর্বশেষ বাস্তব বিশ্বের মান প্রতিফলিত করবে (যেমন বর্তমান status)?
  • কোন ফিল্ড ঐতিহাসিক এবং কখনোও ওভাররাইট করা উচিত নয় (যেমন submitted_at সময়)?
  • কে প্রতিটি ফিল্ড পরিবর্তন করার অনুমতি রাখে (রোল, মালিকানা, অনুমোদন)?
  • মানগুলি সংঘাত করলে সত্যিকারের উৎস কোনটা (ডিভাইস, সার্ভার, ম্যানেজার অনুমোদন)?
  • ভুল সিদ্ধান্ত নিলে কি হয় (ছোট ঝটকা বনাম আর্থিক বা আইনি প্রভাব)?

যখন এই নিয়মগুলো স্পষ্ট, সিঙ্ক কোডের কাজ একটাই: সেগুলো প্রয়োগ করা।

প্রতিটি স্ক্রীনের বদলে প্রতিটি ফিল্ডে মার্জ নিয়ম নির্ধারণ করুন

সংঘাত হলে তা বিরহভাবে পুরো ফর্মকেই একইভাবে প্রভাবিত করে না। একজন ব্যবহারকারী হয়ত একটি ফোন নম্বর আপডেট করেছে যখন অন্যজন একটি নোট যোগ করেছে। যদি আপনি পুরো রেকর্ডকে অল-অর-নাথিং হিসেবে বিবেচনা করেন, আপনি মানুষের ভালো কাজ পুনরায় করতে বাধ্য করবেন।

ফিল্ড-লেভেল মার্জ বেশি পূর্বানুমানযোগ্য কারণ প্রতিটি ফিল্ডের একটি পরিচিত আচরণ থাকে। UX শান্ত এবং দ্রুত থাকে।

শুরু করার একটি সহজ উপায় হলো ফিল্ডগুলোকে "সাধারণত স্বয়ংক্রিয়ভাবে নিরাপদ" এবং "সাধারণত স্বয়ংক্রিয়ভাবে নিরাপদ নয়" বিভাগে আলাদা করা।

সাধারণত স্বয়ংক্রিয়ভাবে মার্জ করা নিরাপদ: notes এবং অভ্যন্তরীণ মন্তব্য, tags, সংযুক্তি (প্রায়ই ইউনিয়ন), এবং last contacted মত টাইমস্ট্যাম্প (প্রায়ই সর্বশেষ রাখা)।

সাধারণত স্বয়ংক্রিয়ভাবে মার্জ করা নিরাপদ নয়: status/state, assignee/owner, totals/prices, approval flags, এবং inventory counts।

তারপর প্রতিটি ফিল্ডে একটি অগ্রাধিকার নিয়ম বেছে নিন। সাধারণ পছন্দগুলো হল server wins, client wins, role wins (উদাহরণ: manager agent-কে ওভাররাইড করে), বা একটি ডিটারমিনিস্টিক টাই-ব্রেকার যেমন নতুনতম সার্ভার ভার্সন।

মূল প্রশ্ন হলো যখন উভয় পক্ষ একই ফিল্ড পরিবর্তন করেছে তখন কি হবে। প্রতিটি ফিল্ডের জন্য একটি আচরণ বেছে নিন:

  • স্পষ্ট নিয়ম নিয়ে অটো-মার্জ (উদাহরণ: tags ইউনিয়ন)\n- উভয় মান রাখুন (উদাহরণ: notes append করে লেখক ও সময়সহ)\n- রিভিউ জন্য ফ্ল্যাগ করুন (উদাহরণ: status এবং assignee-র জন্য একটি পছন্দ দরকার)

উদাহরণ: দুই সাপোর্ট রিপ অফলাইনে একই টিকিট এডিট করে। রিপ A status পরিবর্তন করে Open থেকে Pending এ। রিপ B notes এড করে এবং একটি refund ট্যাগ যোগ করে। সিঙ্কে আপনি নিরাপদে notes এবং tags মার্জ করতে পারেন, কিন্তু status চুপচাপ মার্জ করা ঠিক না — কেবল status-ই প্রম্পট করা উচিত, বাকিগুলো আগেই মার্জ হয়ে থাকবে।

পরে বিতর্ক এড়াতে, প্রতিটি নিয়ম এক বাক্যে ডকুমেন্ট করুন প্রতিটি ফিল্ডের জন্য:

  • "notes: উভয় রাখুন, সর্বশেষটা শেষে append করুন, লেখক ও সময় অন্তর্ভুক্ত করুন."\n- "tags: union, কেবল যদি উভয় পক্ষ আলাদা করে মুছে না দেয় তখনই সরান।"\n- "status: যদি উভয় পক্ষ পরিবর্তন করে, ব্যবহারকারীর পছন্দ বাধ্যতামূলক।"\n- "assignee: manager জিতে, না হলে server জিতে।"

এই এক-বাক্য নিয়মই Kotlin কোড, SQLite কুয়েরি এবং কনফ্লিক্ট UI-এর জন্য সোর্স অফ ট্রুথ হয়ে যায়।

ডেটা মডেল মৌলিক: SQLite-এ ভার্সন এবং অডিট ফিল্ড

সংঘাতগুলো পূর্বানুমানযোগ্য করতে, প্রতিটি সিঙ্কড টেবিলে কয়েকটি মেটাডেটা কলাম যোগ করুন। এগুলো ছাড়া আপনি বলতে পারবেন না যে আপনি কোন সংস্করণ দেখছেন — একটি নতুন এডিট, একটি পুরোনো কপি, না দুইটি এডিট যেগুলো মার্জ দরকার।

প্রায়োগিক ন্যূনতম প্রতিটি সার্ভার-সিঙ্কড রেকর্ডের জন্য:

  • id (স্থিতিশীল প্রাইমারি কি): কখনো reuse করবেন না\n- version (integer): সার্ভারে প্রতিটি সফল লেখায় ইঙ্ক্রিমেন্ট হয়\n- updated_at (timestamp): কখন রেকর্ডটি শেষবার পরিবর্তিত হয়েছিল\n- updated_by (text বা user id): কে শেষবার পরিবর্তন করেছে

ডিভাইসে, এমন লোকাল-ওনলি ফিল্ড যোগ করুন যা কনফার্ম না হওয়া পরিবর্তনগুলো ট্র্যাক করবে:

  • dirty (0/1): লোকাল পরিবর্তন আছে\n- pending_sync (0/1): আপলোডের জন্য কিউ করা হয়েছে, কিন্তু কনফার্ম হয়নি\n- last_synced_at (timestamp): কখন এই রো সার্ভারের সঙ্গে মিলেছে\n- sync_error (text, ঐচ্ছিক): UI-তে দেখানোর জন্য শেষ ব্যর্থতার কারণ

অপটিমিস্টিক কনকারেন্সি হলো একটি সহজ নিয়ম যা নীরব ওভাররাইট প্রতিরোধ করে: প্রতিটি আপডেটে আপনি যে ভার্সনটি এডিট করছেন তা (expected_version) পাঠান। যদি সার্ভার রেকর্ড এখনও সেই ভার্সনে থাকে, আপডেটটি গ্রহণ করা হয় এবং সার্ভার নতুন ভার্সন রিটার্ন করে। না হলে, এটা একটি কনফ্লিক্ট।

উদাহরণ: ব্যবহারকারী A এবং B উভয়ই version = 7 ডাউনলোড করেছে। A আগে সিঙ্ক করে; সার্ভার বাড়িয়ে 8 করে। যখন B expected_version = 7 নিয়ে সিঙ্ক করতে চায়, সার্ভার কনফ্লিক্ট দিয়ে প্রত্যাখ্যান করে যাতে B-এর অ্যাপ ওভাররাইট না করে বরং মার্জ করে।

একটি ভালো কনফ্লিক্ট স্ক্রিন তৈরি করতে, শেয়ার করা শুরু পয়েন্ট সংরক্ষণ করুন: ব্যবহারকারী মূলত কোন রেকর্ড থেকে এডিট করেছিল। দুইটি প্রচলিত পন্থা:

  • শেষ সিঙ্কড রেকর্ডের একটি স্ন্যাপশট সংরক্ষণ করুন (একটি JSON কলাম বা একটি প্যারালাল টেবিল)।\n- একটি চেঞ্জ লগ সংরক্ষণ করুন (প্রতি এডিটের জন্য একটি রো বা ফিল্ড-প্রতি-এডিট)

স্ন্যাপশট সহজ এবং অনেক ক্ষেত্রেই যথেষ্ট। চেঞ্জ লগ ভারী, কিন্তু ঠিক কোনটা পরিবর্তিত হলো তা ফিল্ড-ভিত্তিকভাবে ব্যাখ্যা করতে পারে।

যেকোনভাবে, UI-কে প্রতিটি ফিল্ডের জন্য তিনটি মান দেখাতে সক্ষম হওয়া উচিত: ব্যবহারকারীর এডিট, সার্ভারের বর্তমান মান, এবং শেয়ার করা শুরু পয়েন্ট।

রেকর্ড স্ন্যাপশট বনাম চেঞ্জ লগ: একটি পন্থা বেছে নিন

আপনার অফলাইন-প্রথম অ্যাপ বানান
একটি প্রজেক্টেই স্পষ্ট সিঙ্ক স্টেট এবং সংঘাত হ্যান্ডলিং সহ অফলাইন-প্রথম ফর্ম ফ্লো নির্মাণ করুন।
AppMaster চেষ্টা করুন

অনলাইনে সিঙ্ক করার সময় আপনি পূর্ণ রেকর্ড (স্ন্যাপশট) আপলোড করতে পারেন বা অপারেশনগুলোর একটি তালিকা (চেঞ্জ লগ) আপলোড করতে পারেন। উভয়ই Kotlin ও SQLite-এ কাজ করে, কিন্তু দুইজন মানুষের একই রেকর্ড এডিট করলে আচরণ আলাদা হয়।

অপশন A: পুরো-রেকর্ড স্ন্যাপশট

স্ন্যাপশটের সাথে, প্রতিটি সেভ সর্বশেষ পূর্ণ স্টেট লিখে। সিঙ্কে আপনি রেকর্ড এবং একটি ভার্সন পাঠান। যদি সার্ভার দেখেন ভার্সন পুরোনো, তখন সংঘাত ঘটে।

এটি তৈরি করা সহজ এবং পড়তে দ্রুত, কিন্তু প্রায়ই অনর্থক বড় সংঘাত তৈরি করে। যদি ব্যবহারকারী A ফোন নম্বর পরিবর্তন করে আর ব্যবহারকারী B ঠিকানা পরিবর্তন করে, স্ন্যাপশট পদ্ধতি এক বড় সংঘাত হিসেবে ব্যবস্থা করতে পারে যদিও এডিটগুলো ওভারল্যাপ করে না।

অপশন B: চেঞ্জ লগ (অপারেশন)

চেঞ্জ লগে আপনি কি পরিবর্তন হয়েছে তা সংরক্ষণ করেন, পুরো রেকর্ড নয়। প্রতিটি লোকাল এডিট একটি অপারেশন হয়ে যায় যা সর্বশেষ সার্ভার স্টেটের ওপর রিইপ্লে করা যায়।

যেসব অপারেশন সহজে মার্জ করা যায়:

  • একটি ফিল্ড মান সেট করা (উদা: set email to a new value)\n- একটি নোট append করা (একটি নতুন নোট আইটেম যোগ করে)\n- একটি ট্যাগ যোগ করা (সেটে একটি ট্যাগ যোগ করে)\n- একটি ট্যাগ মুছা (সেট থেকে একটি ট্যাগ মুছে)\n- একটি চেকবক্স সম্পন্ন হিসেবে চিহ্নিত করা (set isDone true সাথে একটি টাইমস্ট্যাম্প)

অপারেশন লগ অনেক ক্ষেত্রেই সংঘাত কমায় কারণ অনেক অ্যাকশন ওভারল্যাপ করে না। নোট append করা সাধারণত অন্য কেউ ভিন্ন নোট append করলে সংঘাত করে না। ট্যাগ যোগ/মুছ সেট ম্যাথের মতো মার্জ করা যায়। সিঙ্গেল-ভ্যালু ফিল্ডের ক্ষেত্রে, যখন দুইটি ভিন্ন এডিট প্রতিদ্বন্দ্বিতা করে তখনও per-field নিয়ম প্রয়োজন।

অবশ্যই ট্রেডঅফ হলো জটিলতা: স্টেবল অপারেশন আইডি, অর্ডারিং (লোকাল সিকোয়েন্স এবং সার্ভার সময়), এবং এমন অপারেশনগুলোর নিয়ম যেগুলো commute করে না — এইগুলোর ব্যবস্থা করতে হয়।

ক্লিনআপ: সফল সিঙ্কের পরে কম্প্যাক্ট করা

অপারেশন লগ বাড়ে, তাই সেগুলো ছোট করার পরিকল্পনা করুন।

একটি সাধারণ পদ্ধতি হলো প্রতিটি রেকর্ডের জন্য কম্প্যাকশন: যখন একটি পরিচিত সার্ভার ভার্সন পর্যন্ত সব অপারেশন স্বীকার করা হয়, সেগুলো একটি নতুন স্ন্যাপশট-এ ফোল্ড করুন এবং পুরোনো অপারেশনগুলো মুছে দিন। যদি আপনাকে undo, অডিটিং বা সহজ ডিবাগিং দরকার হয়, একটি ছোট টেইল রাখুন।

Kotlin + SQLite-এর জন্য ধাপে ধাপে সিঙ্ক ফ্লো

সিঙ্ক-রেডি ডেটা মডেল ডিজাইন করুন
AppMaster-এর ভিজ্যুয়াল Data Designer ব্যবহার করে PostgreSQL ডেটা ভার্সন ও অডিট ফিল্ডসহ মডেল করুন।
বনাতে শুরু করুন

একটি ভাল সিঙ্ক কৌশল মূলত আপনি কি পাঠাচ্ছেন এবং ফিরে কি গ্রহণ করছেন তার প্রতি কড়া হওয়া। লক্ষ্য সহজ: দুর্ঘটনাক্রমে নতুন ডেটা ওভাররাইট করবেন না, এবং যেখানে নিরাপদভাবে মার্জ করা যায় না সেখানে সংঘাতগুলো স্পষ্ট করুন।

প্রায়োগিক ফ্লো:

  1. প্রতিটি এডিট প্রথমে SQLite-এ লিখুন। পরিবর্তনগুলো একটি লোকাল ট্রানজেকশনে সেভ করুন এবং রেকর্ডটি pending_sync = 1 হিসেবে চিহ্নিত করুন। local_updated_at এবং শেষ জানা server_version সংরক্ষণ করুন।

  2. পুরো রেকর্ড নয়, একটি প্যাচ পাঠান। কনেক্টিভিটি ফিরে এলে, রেকর্ড id এবং কেবল যে ফিল্ডগুলো পরিবর্তিত হয়েছে সেগুলো expected_version-এর সাথে পাঠান।

  3. সার্ভার মিসম্যাচ করা ভার্সনগুলো প্রত্যাখ্যান করুক। যদি সার্ভারের বর্তমান ভার্সন expected_version-এর সাথে মিলে না, সার্ভার একটি কনফ্লিক্ট পে-লোড (সার্ভার রেকর্ড, প্রস্তাবিত পরিবর্তন, এবং কোন ফিল্ডগুলো ভিন্ন তা) ফেরত দেয়। যদি ভার্সন মেলে, এটি প্যাচটি প্রয়োগ করে, ভার্সন বাড়ায়, এবং আপডেট করা রেকর্ড ফেরত দেয়।

  4. প্রথমে অটো- মার্জ প্রয়োগ করুন, তারপর ব্যবহারকারীকে জিজ্ঞাসা করুন। ফিল্ড-লেভেল মার্জ নিয়ম চালান। নিরাপদ ফিল্ডগুলোকে notes মত এবং সংবেদনশীল ফিল্ডগুলোকে status, price, বা assignee আলাদা বিবেচনা করুন।

  5. চূড়ান্ত ফলাফল commit করে pending ফ্ল্যাগগুলো সরান। তা অটো-মার্জই হোক বা ম্যানুয়ালি রেজলভ করা হোক, চূড়ান্ত রেকর্ড SQLite-এ লিখুন, server_version আপডেট করুন, pending_sync = 0 সেট করুন, এবং পরে কি ঘটেছে তা ব্যাখ্যা করার জন্য পর্যাপ্ত অডিট ডেটা নথিভুক্ত করুন।

উদাহরণ: দুই সেলস রিপ অফলাইনে একই অর্ডার এডিট করে। রিপ A ডেলিভারি তারিখ পরিবর্তন করে। রিপ B গ্রাহকের ফোন নম্বর পরিবর্তন করে। প্যাচস দিয়ে সার্ভার উভয় পরিবর্তনকে পরিষ্কারভাবে গ্রহণ করতে পারে। যদি উভয়েই ডেলিভারি তারিখ পরিবর্তন করে, আপনি একটি স্পষ্ট সিদ্ধান্ত উপস্থাপন করবেন পরিবর্তে পুরো রেকর্ড আবার ধরাতে বাধ্য করার।

UI প্রতিশ্রুতি স্থির রাখুন: "Saved" মানে লোকালি সেভ করা। "Synced" আলাদা, স্পষ্ট স্টেট হওয়া উচিৎ।

ফর্মে সংঘাত রিজলভ করার UX প্যাটার্ন

সংঘাতগুলো এক্সসেপশন হওয়া উচিত, নিয়ম নয়। প্রথমে যা নিরাপদ তা অটো-মার্জ করুন, তারপর কেবলমাত্র যখন সিদ্ধান্ত সত্যিই প্রয়োজন তখনই ব্যবহারকারীকে জিজ্ঞাসা করুন।

নিরাপদ ডিফল্ট দিয়ে সংঘাত বিরল করে তোলুন

যদি দুইজন ভিন্ন ফিল্ডে এডিট করে, একটি মডাল দেখানোর পরিবর্তে নীরবে মার্জ করুন। উভয় পরিবর্তন রাখুন এবং একটি ছোট "Updated after sync" মেসেজ দেখান।

প্রম্পটগুলো রাখুন শুধুমাত্র প্রকৃত সংঘর্ষের জন্য: একই ফিল্ড উভয় ডিভাইসে পরিবর্তিত হলে, বা কোন পরিবর্তন অন্য একটি ফিল্ডের উপর নির্ভর করে (যেমন status এবং status reason)।

যখন জিজ্ঞাসা করা জরুরি, দ্রুত শেষ করা সহজ করুন

একটি কনফ্লিক্ট স্ক্রিনকে দুটি জিনিস উত্তর দেয়া উচিত: কি পরিবর্তন হয়েছে, এবং কি সেভ হবে। মানগুলো পার্শ্বে পার্শ্বে তুলনা করুন: "Your edit", "Their edit", এবং "Saved result"। যদি কেবল দুটি ফিল্ড সংঘর্ষ করে, পুরো ফর্ম দেখাবেন না। সোজা সেই ফিল্ডগুলোতে যান এবং বাকি অংশগুলো read-only রাখুন।

কর্মগুলো সীমিত রাখুন যা মানুষ সত্যিই চাইবে:

  • Keep mine\n- Keep theirs\n- Edit final\n- Review field-by-field (প্রয়োজন হলে কেবল)

আংশিক মার্জই UX-কে গোলমেলে করে। কেবল সংঘাতপূর্ণ ফিল্ডগুলো হাইলাইট করুন এবং উত্স স্পষ্টভাবে লেবেল করুন ("Yours" এবং "Theirs")। নিরাপদ অপশনটি প্রি-সিলেক্ট করে দিন যাতে ব্যবহারকারী কনফার্ম করে দ্রুত এগোতে পারে।

ব্যবহারকারীকে না আটকে রাখার জন্য প্রত্যাশা সেট করুন। বলুন তারা গেলে কি হবে: উদাহরণ, "আমরা আপনার ভার্সন লোকালি রাখব এবং পরে পুনরায় সিঙ্ক চেষ্টা করব" অথবা "এই রেকর্ড Needs review-এ থাকবে যতক্ষণ না আপনি পছন্দ করেন।" সেই স্টেটটি লিস্টে দৃশ্যমান রাখুন যাতে সংঘাতগুলো হারিয়ে না যায়।

আপনি যদি এই ফ্লোটি AppMaster-এ তৈরি করেন, একই UX পদ্ধতি প্রযোজ্য: প্রথমে নিরাপদ ফিল্ডগুলো অটো-মার্জ করুন, তারপর কেবল নির্দিষ্ট ফিল্ড সংঘর্ষ হলে একটি ফোকাসড রিভিউ ধাপ দেখান।

জটিল কেস: ডিলেট, ডুপ্লিকেট এবং "নিখোঁজ" রেকর্ড

মার্জ নিয়মগুলো কার্যকর করুন
আপনার per-field মার্জ নিয়মগুলোকে একটি drag-and-drop Business Process হিসেবে ব্যাকএন্ড লজিকে রূপান্তর করুন।
প্রজেক্ট তৈরি করুন

সিঙ্ক সমস্যার বেশিরভাগ যেটা আকস্মিক মনে হয় তা তিনটি অবস্থা থেকে আসে: কেউ ডিলেট করেছে যখন অন্য কেউ এডিট করছে, দুই ডিভাইস অফলাইনে একই জিনিস তৈরি করেছে (ডুপ্লিকেট), অথবা একটি রেকর্ড অদৃশ্য হয়ে পরে আবার পুনরায় দেখা যায়। এগুলোর জন্য স্পষ্ট নিয়ম দরকার কারণ LWW প্রায়ই মানুষকে চমকে দেয়।

Delete বনাম edit: কোনটা জিতবে?

নির্ধারণ করুন ডিলেট কি এডিটের চেয়ে শক্ত। অনেক ব্যবসায়িক অ্যাপে ডিলেট জিতবে কারণ ব্যবহারকারীরা আশা করবে একটি মুছে ফেলা রেকর্ডটিই মুছে থাকবে।

প্রায়োগিক নিয়মগুচ্ছ:

  • যদি কোনো ডিভাইসে রেকর্ডটি ডিলিট হয়ে থাকে, সেটি সর্বত্র ডিলিট হিসেবে বিবেচনা করুন, এমনকি পরে এডিট এলেও।\n- যদি ডিলেটগুলো reversible হতে হয়, তখন হার্ড ডিলেটের বদলে deleted_at দিয়ে আর্কাইভ করুন।\n- যদি একটি এডিট মুছে ফেলা রেকর্ডের জন্য আসে, সেটি অডিট হিস্টরিতে রাখুন কিন্তু রেকর্ড পুনরুদ্ধার করবেন না।

অফলাইন তৈরি সংঘর্ষ এবং ডুপ্লিকেট ড্রাফট

অফলাইন-প্রথম ফর্মগুলো প্রায়ই সার্ভার চূড়া আইডি দেওয়ার আগে সাময়িক ID (যেমন UUID) তৈরি করে। ডুপ্লিকেট তখন ঘটে যখন ব্যবহারকারীরা একই বাস্তব জিনিসের জন্য দুটি ড্রাফট তৈরি করে।

যদি আপনার কাছে একটি স্থিতিশীল ন্যাচারাল কি থাকে (receipt number, barcode, email+date), সেটি collision সনাক্ত করতে ব্যবহার করুন। না থাকলে, ডুপ্লিকেট হওয়াই হবে এবং পরে এক সহজ মার্জ অপশন দিন।

বাস্তবায়নের টিপ: SQLite-এ local_id এবং server_id উভয়ই সংরক্ষণ করুন। সার্ভার রেসপন্সের সময় একটি ম্যাপিং লিখুন এবং যতক্ষণ পর্যন্ত না আপনি নিশ্চিত যে কোনো কিউ করা পরিবর্তন আর লোকাল ID-টি রেফারেন্স করে না ততক্ষণ এটি রাখুন।

সিঙ্কের পরে পুনরুত্থান (resurrection) প্রতিরোধ

রেসারেকশন হয় যখন ডিভাইস A রেকর্ড মুছে দেয়, কিন্তু ডিভাইস B অফলাইনে থাকায় পুরোনো কপি আপলোড করলে এটি পুনরায় তৈরি হয়ে যায়।

ফিক্স হলো টোম্বস্টোন। রো সরিয়ে ফেলার বদলে সেটি deleted_at (সাধারণত deleted_by এবং delete_version সহ) দিয়ে চিহ্নিত করুন। সিঙ্কের সময় টোম্বস্টোনগুলোকে বাস্তব পরিবর্তন হিসেবে বিবেচনা করুন যা পুরোনো নন-ডিলিটেড স্টেটও ওভাররাইড করতে পারে।

টোম্বস্টোন কতদিন রাখবেন তা নির্ধারণ করুন। যদি ব্যবহারকারীরা সপ্তাহগুলো খোলা থাকতে পারে অফলাইনে, টোম্বস্টোনগুলোও ততটা সময় রাখুন। কেবল তখনই purge করুন যখন আপনি নিশ্চিত যে সক্রিয় ডিভাইসগুলো ডিলেটের পরবর্তী ভার্সন পর্যন্ত সিঙ্ক করেছে।

আপনি যদি undo সাপোর্ট করেন, undo-কে আরেকটি পরিবর্তন হিসেবে বিবেচনা করুন: deleted_at পরিষ্কার করুন এবং ভার্সন বাড়ান।

ডেটা লস বা ব্যবহারকারী হতাশা সৃষ্টিকারী সাধারণ ভুলগুলো

বাস্তব সোর্স কোড আউটপুট পান
প্রয়োজন হলে Kotlin, SwiftUI, Go, এবং Vue3-এর বাস্তব সোর্স কোড এক্সপোর্ট করুন।
এখন তৈরি করুন

অনেক সিঙ্ক ব্যর্থতা ছোট ধারনার ফলে হয় যা ধীরে ধীরে ভালো ডেটা মুছে দেয়।

ভুল ১: এডিটগুলো অর্ডার করতে ডিভাইস টাইমে বিশ্বাস করা

ফোনের ক্লক ভুল হতে পারে, টাইমজোন বদলে যায়, এবং ব্যবহারকারী এসময় হাতে টাইম ম্যানুয়ালি সেট করতে পারে। যদি আপনি ডিভাইস টাইম দিয়ে এডিটগুলো অর্ডার করেন, আপনি শেষমেষ ভুল ক্রমে এডিট প্রয়োগ করবেন।

অগ্রাধিকার দিন সার্ভার-ইস্যুকৃত ভার্সনের (serverVersion) এবং ক্লায়েন্ট টাইমস্ট্যাম্পকে শুধুমাত্র প্রদর্শনের জন্য রাখুন। যদি সময় ব্যবহার জরুরি হয়, সার্ভারে safeguards যোগ করুন।

ভুল ২: সংবেদনশীল ফিল্ডে দুর্ঘটনাক্রমে LWW ব্যবহার

LWW সহজ মনে হলেও সংবেদনশীল ফিল্ডে এটি সমস্যা করে যখন পরে সিঙ্ক করা ব্যবহারকারী জিতে যায়। status, totals, approvals, এবং assignments সাধারণত explicit নিয়ম বা ম্যানুয়াল রিভিউ প্রয়োজন।

উচ্চ-ঝুঁকির ফিল্ডের জন্য একটি নিরাপত্তা চেকলিস্ট:

  • স্ট্যাটাস ট্রানজিশনগুলোকে state machine হিসেবে বিবেচনা করুন, ফ্রি-টেক্সট এডিট হিসেবে নয়।\n- টোটালগুলো line items থেকে পুনঃগণনা করুন; কাঁচা সংখ্যার মত মার্জ করবেন না।\n- কাউন্টারগুলোর জন্য ডেলটা প্রয়োগ করে মার্জ করুন, জেতানো নয়।\n- ownership বা assignee-র সংঘাতে স্পষ্ট কনফার্মেশন দাবি করুন।

ভুল ৩: স্টেইলে থাকা পুরানো কপি দিয়ে নতুন সার্ভার ভ্যালুকে ওভাররাইট করা

এটি ঘটে যখন ক্লায়েন্ট একটি পুরোনো স্ন্যাপশট এডিট করে, তারপর পুরো রেকর্ড আপলোড করে। সার্ভার তা গ্রহণ করে এবং নতুন সার্ভার-সাইড পরিবর্তনগুলো অদৃশ্য হয়ে যায়।

আপনি যা পাঠান তার আকার ঠিক করুন: কেবল পরিবর্তিত ফিল্ড (বা একটি চেঞ্জ লগ) পাঠান, প্লাস বেস ভার্সন। যদি বেস ভার্সন পিছিয়ে থাকে, সার্ভার প্রত্যাখ্যান বা মার্জ বাধ্য করবে।

ভুল ৪: কে কি পরিবর্তন করেছে এমন ইতিহাস না রাখা

সংঘাত হলে ব্যবহারকারীরা জানতে চায়: আমি কি পরিবর্তন করেছিলাম, এবং অন্য ব্যক্তি কি পরিবর্তন করেছে? editor identity এবং per-field পরিবর্তন ছাড়া, আপনার কনফ্লিক্ট স্ক্রিন অনুমানের ওপর ভিত্তি করে হবে।

updatedBy, সার্ভার-সাইড update সময় (যদি থাকে), এবং অন্তত একটি হালকা per-field অডিট ট্রেইল সংরক্ষণ করুন।

ভুল ৫: পুরো-রেকর্ড তুলনা বাধ্যতামূলক করা এমন কনফ্লিক্ট UI

ব্যক্তিদের পুরো রেকর্ড তুলনা করানো ক্লান্তিকর। বেশিরভাগ সংঘাত শুধুমাত্র ১-৩ ফিল্ডের। কেবল সংঘাতপূর্ণ ফিল্ডগুলো দেখান, নিরাপদ অপশনটি প্রি-সিলেক্ট করুন, এবং বাকি স্বয়ংক্রিয়ভাবে قبول করুন।

আপনি যদি AppMaster বা অন্য কোনো নো-কোড টুলে ফর্ম তৈরি করে থাকেন, একই লক্ষ্য রাখুন: ফিল্ড-লেভেলে সংঘাত সমাধান করুন যাতে ব্যবহারকারীরা একবারে একটি স্পষ্ট সিদ্ধান্ত নিতে পারে সম্পূর্ণ ফর্ম স্ক্রল না করে।

দ্রুত চেকলিস্ট এবং পরবর্তী ধাপ

আপনি যদি চান যে অফলাইন এডিটগুলো নিরাপদ অনুভব করুক, সংঘাতকে একটি স্বাভাবিক অবস্থা হিসেবে বিবেচনা করুন, ত্রুটি হিসেবে নয়। ভাল ফলাফল আসে স্পষ্ট নিয়ম, পুনরাবৃত্ত পরীক্ষার, এবং UI যা সহজ ভাষায় কি হয়েছিল তা ব্যাখ্যা করে।

ফিচার যোগ করার আগে এই বেসিকগুলি নিশ্চিত করুন:

  • প্রতিটি রেকর্ড টাইপের জন্য প্রতিটি ফিল্ডে একটি মার্জ রুল বরাদ্দ করুন (LWW, keep max/min, append, union, বা সর্বদা জিজ্ঞাসা)।\n- একটি সার্ভার-কন্ট্রোলড version এবং একটি updated_at রাখুন এবং সিঙ্কে তাদের যাচাই করুন।\n- দুই-ডিভাইস টেস্ট চালান যেখানে উভয়ই একই রেকর্ড অফলাইনে এডিট করে, তারপর উভয় ক্রমেই সিঙ্ক করান (A তখন B, B তখন A)। ফলাফল নির্ধারণযোগ্য হওয়া উচিত।\n- কঠিন সংঘাতগুলো টেস্ট করুন: delete vs edit, এবং বিভিন্ন ফিল্ডে edit vs edit।\n- রাষ্ট্র স্পষ্ট করুন: Synced, Pending upload, এবং Needs review দেখান।

একটি বাস্তব ফর্ম নিয়ে পুরো ফ্লো end-to-end প্রোটোটাইপ করুন, ডেমো স্ক্রিন নয়। একটি বাস্তব পরিস্থিতি ব্যবহার করুন: একজন ফিল্ড টেক ফোনে একটি জব নোট আপডেট করে যখন একজন ডিসপ্যাচার একই জবের শিরোনাম ট্যাবলেটে এডিট করে। যদি তারা ভিন্ন ফিল্ড স্পর্শ করে, অটো-মার্জ করে এবং একটি ছোট "Updated from another device" হিন্ট দেখান। যদি তারা একই ফিল্ড স্পর্শ করে, একটি সরল রিভিউ স্ক্রিনে রুট করুন যেখানে দুইটি অপশন এবং একটি পরিষ্কার প্রিভিউ আছে।

যখন আপনি পূর্ণ মোবাইল অ্যাপ এবং ব্যাকএন্ড API একসাথে তৈরি করতে প্রস্তুত, AppMaster (appmaster.io) সাহায্য করতে পারে। আপনি ডেটা মডেল করতে, ব্যবসায়িক লজিক নির্ধারণ করতে, এবং ওয়েব ও নেটিভ মোবাইল UI এক জায়গায় বানাতে পারবেন, তারপর সিঙ্ক নিয়মগুলো যখন কঠোর মনে হবে তখন ডেপ্লয় বা সোর্স কোড এক্সপোর্ট করতে পারবেন।

প্রশ্নোত্তর

What is an offline sync conflict, in plain terms?

সংঘাত ঘটে যখন দুইটি ডিভাইস একই সবার-ব্যাকড রেকর্ডটি পরিবর্তন করে যখন তারা অফলাইনে থাকে (অথবা উভয়ই এখনও সিঙ্ক করে নাই), এবং সার্ভার পরে দেখে যে উভয় আপডেটই একটি পুরনো ভার্সনের উপর ভিত্তি করে। তখন সিস্টেমকে প্রতিটি ভিন্ন ফিল্ডের জন্য চূড়ান্ত মান নির্ধারণ করতে হয়।

Which conflict strategy should I choose: last write wins, manual review, or field-level merge?

সাধারণত অধিকাংশ ব্যবসায়িক ফর্মের জন্য ডিফল্ট হিসেবে ফিল্ড-লেভেল মার্জ দিয়ে শুরু করুন, কারণ বিভিন্ন ভূমিকা আলাদা অংশ আপডেট করে এবং আপনি অধিকাংশ ক্ষেত্রে উভয় পরিবর্তনই রাখতে পারেন কোনোজনকে বিরক্ত না করে। কেবলমাত্র সেসব ফিল্ডের জন্য ম্যানুয়াল রিভিউ ব্যবহার করুন যেগুলো ভুল অনুমান করলে বড় ক্ষতি করতে পারে (টাকা, অনুমোদন, কমপ্লায়েন্স)। Last write wins কেবল নিম্ন-ঝুঁকির ক্ষেত্রের জন্য ব্যবহার করুন যেখানে পুরানো এডিট হারানো গ্রহণযোগ্য।

When should the app ask the user to resolve a conflict?

যদি দুইটি এডিট ভিন্ন ফিল্ডে স্পর্শ করে, আপনি সাধারণত স্বয়ংক্রিয়ভাবে মার্জ করতে পারবেন এবং UI শান্ত রাখা যায়। যদি দুইটি এডিট একই ফিল্ডকে ভিন্নভাবে পরিবর্তন করে, সেই ক্ষেত্রটিই সিদ্ধান্তের জন্য ট্রিগার করা উচিত, কারণ স্বয়ংক্রিয় সিদ্ধান্ত কারো কাছে অপ্রত্যাশিত হতে পারে। সিদ্ধান্তের স্কোপ ছোট রাখুন — কেবল সংঘাতপূর্ণ ফিল্ডগুলোই দেখান, পুরো ফর্ম নয়।

How do record versions prevent silent overwrites?

version-কে সার্ভারের মনোটনিক কাউন্টার হিসেবে বিবেচনা করুন এবং প্রতিটি আপডেটের সাথে ক্লায়েন্টকে একটি expected_version পাঠাতে বাধ্য করুন। যদি সার্ভারের বর্তমান ভার্সন মেলে না, তবে ওভাররাইট না করে কনফ্লিক্ট রেসপন্স দিয়ে প্রত্যাখ্যান করুন। এই নিয়মটি “নীরব ডেটা লস” প্রতিরোধ করে, এমনকি যখন দুই ডিভাইস আলাদা ক্রমে সিঙ্ক করে।

What metadata should every synced SQLite table include?

প্রতিটি সার্ভার-সিঙ্কড টেবিলে একটি স্থিতিশীল id, সার্ভার-নিয়ন্ত্রিত version, এবং সার্ভার-নিয়ন্ত্রিত updated_at/updated_by অন্তর্ভুক্ত করা উচিত যাতে আপনি কি পরিবর্তিত হয়েছে তা ব্যাখ্যা করতে পারেন। ডিভাইসে, ধরুন pending_sync দিয়ে রো-টা আপলোড অপেক্ষায় আছে কিনা ট্র্যাক করুন এবং শেষ সিঙ্ক করা সার্ভার ভার্সন রাখুন। এগুলো ছাড়া আপনি কনফ্লিক্ট নির্ণয় বা সহায়ক রেজোলিউশন স্ক্রিন দেখাতে পারবেন না।

Should I sync the whole record or only changed fields?

প্যাচ — কেবলমাত্র যে ফিল্ডগুলো পরিবর্তিত হয়েছে সেগুলো পাঠান, সাথে আপনার বেস expected_version। পুরো-রেকর্ড আপলোড ছোট, অ-ওভারল্যাপিং এডিটগুলোকে অপ্রয়োজনীয় সংঘাতে পরিণত করে এবং স্টেইলে থাকা পুরোনো ভ্যালু দিয়ে নতুন সার্ভার ভ্যালুকে ওভাররাইট করার ঝুঁকি বাড়ায়। প্যাচগুলি কোন ফিল্ড মার্জ রুল দরকার তা واضح রাখে।

Is it better to store snapshots or a change log for offline edits?

স্ন্যাপশট সহজ: আপনি সর্বশেষ পূর্ণ রেকর্ড সংরক্ষণ করেন এবং পরে সার্ভারের সঙ্গে তুলনা করেন। চেঞ্জ লগ বেশি নমনীয়: আপনি অপারেশনগুলো (যেমন “ফিল্ড সেট করা” বা “নোট অ্যাপেন্ড”) সংরক্ষণ করে সেগুলোকে সর্বশেষ সার্ভার স্টেটের ওপর পুনরায় প্লে করেন, যা নোট, ট্যাগ এবং অন্যান্য অ্যাডিটিভ আপডেটে সাধারণত ভালভাবে মার্জ হয়। দ্রুত বাস্তবায়নের জন্য স্ন্যাপশট বেছে নিন; মার্জ ঘনঘন হলে এবং “কে কি পরিবর্তন করেছে” পরিষ্কার রাখতে চাইলে চেঞ্জ লগ নিন।

How should I handle delete vs edit conflicts?

আপনি আগে থেকে নির্ধারণ করুন যে ডিলেট কি এডিটকে ওভাররাইড করবে কি না, কারণ ব্যবহারকারীরা ধারাবাহিক আচরণ আশা করে। অনেক ব্যবসায়িক অ্যাপে নিরাপদ ডিফল্ট হলো ডিলেটকে টোম্বস্টোন হিসেবে আচরণ করা (রেকর্ডটি deleted_at দিয়ে চিহ্নিত করা) যাতে একটি পুরনো অফলাইন আপসার্ট এটিকে পুনরায় সৃষ্টি না করে। যদি রিভার্সিবিলিটি দরকার হয়, কঠোর ডিলেটের বদলে “archived” স্টেট ব্যবহার করুন।

What are the most common mistakes that cause offline sync data loss?

ডিভাইস ক্লক ভুল হতে পারে; টাইমজোন বদলায় এবং ব্যবহারকারী সময় ম্যানুয়ালি বদলাতে পারে। যদি আপনি ডিভাইস টাইম দিয়ে এডিট অর্ডার নির্ভর করেন, ক্রমে ভুল লাগানো হবে। পরিবর্তে সার্ভার-ইস্যুকৃত ভার্সন ব্যবহার করুন (মনোটোনিক serverVersion) এবং ক্লায়েন্ট টাইমস্ট্যাম্পকে শুধুমাত্র প্রদর্শনের জন্য রাখুন।

How can I implement a conflict-friendly UX when building with AppMaster?

সংক্ষিপ্তভাবে: প্রতিটি রেকর্ড টাইপের জন্য প্রতিটি ফিল্ডে একটি মার্জ রুল নির্ধারণ করুন (LWW, keep max/min, append, union, বা সর্বদা জিজ্ঞাসা)। একটি সার্ভার-কন্ট্রোলড version এবং একটি updated_at রাখুন এবং সিঙ্কের সময়কার যাচাই করুন। দুই-ডিভাইস টেস্ট চালান যেখানে উভয়ই একই রেকর্ড অফলাইনে এডিট করে, এবং উভয় ক্রমে (A ثم B, B ثم A) সিঙ্ক করুন। কঠিন কনফ্লিক্টগুলো টেস্ট করুন: delete vs edit, এবং বিভিন্ন ফিল্ডে edit vs edit। রাষ্ট্র স্পষ্ট রাখুন: Synced, Pending upload, এবং Needs review দেখান।

শুরু করা সহজ
কিছু আশ্চর্যজনকতৈরি করুন

বিনামূল্যের পরিকল্পনা সহ অ্যাপমাস্টারের সাথে পরীক্ষা করুন।
আপনি যখন প্রস্তুত হবেন তখন আপনি সঠিক সদস্যতা বেছে নিতে পারেন৷

এবার শুরু করা যাক