আপনি যে মুহূর্তটি জানেন যখন আপনি কোনও জায়গায় আছেন, আপনার 4G "ই" হয়ে যায়, এবং আপনার মোবাইল অ্যাপ্লিকেশনটি একটি খুব ব্যয়বহুল কাগজের ওজন হওয়ার সিদ্ধান্ত নেয়? একটি ড্রাইভার ড্রাইভার একটি লোড স্পিনারের দিকে তাকিয়ে চিন্তা করুন কারণ অ্যাপ্লিকেশন ইন্টারনেট ছাড়া তাদের রুট লোড করতে পারে না। আমাদের ক্লায়েন্টদের অনেকগুলি এই সংযোগের খোঁজ পেয়েছে - দূরবর্তী চাকরির সাইটগুলি, বহিরাগত ইভেন্টগুলি, স্টোরেজ মেঝে, লজিস্টিক হাবস। মূলত, যেখানেই Wi-Fi মারা যায়। এই ছোট আকর্ষণ, আমি আপনাকে দেখাবো কিভাবে আমি একটি স্থানীয়-প্রথম মোবাইল অ্যাপ্লিকেশন তৈরি করেছি আপনি দেখতে পাবেন: React Native + RxDB কিভাবে আপনার সার্ভারটি মিশ্রিত না করে দুই পথের সিনেকশন সত্যিই কাজ করে। কিভাবে ডেটা সংঘর্ষের মতো "মজার" পরিস্থিতি মোকাবেলা করবেন এই প্রাণীগুলির মধ্যে একটি ডিজাইন করার সময় কি ভুলবেন না। এছাড়াও - আমি একটি "শেষ আপডেট জয়" ট্রিক শেয়ার করব. সবসময় সঠিক পছন্দ নয়, কিন্তু আমাদের ক্ষেত্রে ... চাচার চুমু। The Stack (a.k.a. My Chosen Weapons) আমার নির্বাচিত অস্ত্র (My Chosen Weapons) এই নির্মাণ জন্য, আমি rolled সঙ্গে: React Native - ক্রস-প্ল্যাটফর্ম জাদু: একটি কোড বেস, আইওএস + অ্যান্ড্রয়েড react-native-nitro-sqlite - কারণ SQLite ছাড়া স্থানীয় স্টোরেজ চিনি ছাড়া পিজার মত। RxDB — অফলাইন-প্রথম, প্রতিক্রিয়াশীল DB যা সমন্বয় সঙ্গে ভাল খেলছে। NestJS + TypeORM + PostgreSQL - ব্যাকেন্ড স্বপ্ন দল। চূড়ান্ত ফলাফল: অ্যাপ্লিকেশন অফলাইনে কাজ করে, পরে সমন্বয় করে, এবং একটি মল্টাউন ছাড়াই অদৃশ্য সংযোগগুলি বেঁচে থাকে। Step 1 — Local Storage via SQLite ধাপ 1 - SQLite এর মাধ্যমে স্থানীয় স্টোরেজ প্রথমে, আমার একটি স্থানীয় ডাটাবেসের দরকার ছিল যা RxDB আনন্দে অপব্যবহার করতে পারে. SQLite স্পষ্ট পছন্দ, কিন্তু আমি এটি কিছু অতিরিক্ত ক্ষমতা - যাচাইকরণ এবং এনক্রিপশন সঙ্গে প্যাক করেছি - কারণ ডেটা সম্পূর্ণতা এবং গোপনীয়তা গুরুত্বপূর্ণ (এবং কারণ আমার ভবিষ্যত নিজের আমাকে ধন্যবাদ দেবে)। //storage.ts import { getRxStorageSQLiteTrial, getSQLiteBasicsQuickSQLite, } from 'rxdb/plugins/storage-sqlite'; import { open } from 'react-native-nitro-sqlite'; import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv'; import { wrappedKeyEncryptionCryptoJsStorage } from 'rxdb/plugins/encryption-crypto-js'; const sqliteBasics = getSQLiteBasicsQuickSQLite(open); const storage = getRxStorageSQLiteTrial({ sqliteBasics }); const validatedStorage = wrappedValidateAjvStorage({ storage }); const encryptedStorage = wrappedKeyEncryptionCryptoJsStorage({ storage: validatedStorage, }); export { encryptedStorage }; হ্যাঁ, এটি প্যাকেজিংয়ের তিনটি স্তর। একটি স্যুপের মতো. অথবা অনেক মিডেলওয়্যার স্তর সহ একটি উদ্যোগ। Step 2 — Creating the Database Instance ধাপ 2 - ডাটাবেস ইনস্ট্যান্স তৈরি করুন এরপর আমি একটি নির্মাণ করেছি। কারণ আপনি যদি মনে করেন যে আপনার ডিবি এর একাধিক ইন্সটাফরেক্স একটি ভাল ধারণা ... আপনি সম্ভবত উৎপাদনের মধ্যে সংঘাতও উপভোগ করেন। RxDatabaseManager এখানে তার সমস্ত গৌরবের ক্লাস: //Instance.ts import { addRxPlugin, createRxDatabase, RxDatabase, WithDeleted } from 'rxdb'; import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode'; import { replicateRxCollection } from 'rxdb/plugins/replication'; import NetInfo from '@react-native-community/netinfo'; import { CheckPointType, MyDatabaseCollections, ReplicateCollectionDto, } from './types.ts'; import { encryptedStorage } from './storage.ts'; import { defaultConflictHandler } from './utills.ts'; import { usersApi, userSchema, UserType } from '../features/users'; import { RxDBMigrationSchemaPlugin } from 'rxdb/plugins/migration-schema'; import { RxDBQueryBuilderPlugin } from 'rxdb/plugins/query-builder'; import { RxDBUpdatePlugin } from 'rxdb/plugins/update'; // for support query.update method addRxPlugin(RxDBUpdatePlugin); // for support chained query methods addRxPlugin(RxDBQueryBuilderPlugin); // for enabling data migration addRxPlugin(RxDBMigrationSchemaPlugin); export class RxDatabaseManager { private static instance: RxDatabaseManager; private db: RxDatabase<MyDatabaseCollections> | null = null; private isOnline = false; private constructor() {} public static getInstance(): RxDatabaseManager { if (!RxDatabaseManager.instance) { RxDatabaseManager.instance = new RxDatabaseManager(); } return RxDatabaseManager.instance; } public async init(): Promise<RxDatabase<MyDatabaseCollections>> { if (this.db) return this.db; if (__DEV__) { // needs to be added in dev mode addRxPlugin(RxDBDevModePlugin); } this.db = await createRxDatabase<MyDatabaseCollections>({ name: 'myDb', storage: encryptedStorage, multiInstance: false, // No multi-instance support for React Native closeDuplicates: true, // Close duplicate database instances }); await this.db.addCollections({ users: { schema: userSchema, conflictHandler: defaultConflictHandler, migrationStrategies: { // 1: function (oldDoc: UserType) {}, }, }, }); this.setupConnectivityListener(); return this.db; } public getDb(): RxDatabase<MyDatabaseCollections> { if (!this.db) { throw new Error('Database not initialized. Call init() first.'); } return this.db; } private replicateCollection<T>(dto: ReplicateCollectionDto<T>) { const { collection, replicationId, api } = dto; const replicationState = replicateRxCollection<WithDeleted<T>, number>({ collection: collection, replicationIdentifier: replicationId, pull: { async handler(checkpointOrNull: unknown, batchSize: number) { const typedCheckpoint = checkpointOrNull as CheckPointType; const updatedAt = typedCheckpoint ? typedCheckpoint.updatedAt : 0; const id = typedCheckpoint ? typedCheckpoint.id : ''; const response = await api.pull({ updatedAt, id, batchSize }); return { documents: response.data.documents, checkpoint: response.data.checkpoint, }; }, batchSize: 20, }, push: { async handler(changeRows) { console.log('push'); const response = await api.push({ changeRows }); return response.data; }, }, }); replicationState.active$.subscribe(v => { console.log('Replication active$:', v); }); replicationState.canceled$.subscribe(v => { console.log('Replication canceled$:', v); }); replicationState.error$.subscribe(async error => { console.error('Replication error$:', error); }); } private async startReplication() { const db = this.getDb(); this.replicateCollection<UserType>({ collection: db.users, replicationId: '/users/sync', api: { push: usersApi.push, pull: usersApi.pull, }, }); } private setupConnectivityListener() { NetInfo.addEventListener(state => { const wasOffline = !this.isOnline; this.isOnline = Boolean(state.isConnected); if (this.isOnline && wasOffline) { this.onReconnected(); } }); } private async onReconnected() { this.startReplication(); } } এই ছোট্ট প্রাণী: ডিবি ডাউনলোড করুন ইন্টারনেটের জন্য ঘড়ি যেন একটি ক্লিং প্রাক্তন। আমরা যখন অনলাইনে ফিরে আসি তখন সিনাক্স। এবং হ্যাঁ, এটি সবকিছু লগ করে. ভবিষ্যত আমি পরবর্তী বার ডাবগিং করার সময় কৃতজ্ঞ হব। Step 3 — Bootstrapping the DB When the App Starts ধাপ 3 — অ্যাপ্লিকেশন চালু হওয়ার সময় ডিবি বুটস্ট্র্যাপিং অ্যাপ্লিকেশনটি চালু হলে, আমি আমার এখানে কোনও জাদু নেই – শুধু ভাল ওল’ নিজের কাজ করে। RxDatabaseManager useEffect init সময় কিছু বিস্ফোরিত হলে, আমি এটি লগ করি. কারণ ত্রুটিগুলি অস্তিত্ব না করে এমনভাবে আপনি অ্যাপগুলি আঘাত পেতে পারেন। //App.tsx useEffect(() => { const init = async () => { const dbManager = RxDatabaseManager.getInstance(); dbManager .init() .then(() => { setAppStatus('ready'); }) .catch((error) => { console.log('Error initializing database:', error); setAppStatus('error'); }); }; init(); }, []); Step 4 — Data Replication (a.k.a. Syncing Without Tears) ধাপ 4 – ডাটা রিপ্লিকেশন (একটি ডাটা সিনাক্সিং ছাড়া কান্না) যখন অ্যাপ্লিকেশনটি "অফলাইন গুহা মোড" থেকে অনলাইনে ফিরে যায়, যা লোকাল ডিবি এবং সার্ভারের মধ্যে ডেটা সিনাক্স বন্ধ করে দেয়। . onReconnected() replicateRxCollection এখানে একটি মৌলিক pull handler - RxDB একটি চেকপয়েন্ট পাঠায় ( , ) তাই সার্ভার জানে আমরা কোথায় থেমে গেছি. কারণ কেউ পুনরুদ্ধার করতে চায় না প্রতিবারই ডিবি। updatedAt id পুরো //instance.ts async handler(checkpointOrNull: unknown, batchSize: number) { const typedCheckpoint = checkpointOrNull as CheckPointType; const updatedAt = typedCheckpoint ? typedCheckpoint.updatedAt : 0; const id = typedCheckpoint ? typedCheckpoint.id : ''; const response = await api.pull({ updatedAt, id, batchSize }); return { documents: response.data.documents, checkpoint: response.data.checkpoint, }; }, সার্ভারে, আমি শুধুমাত্র সর্বশেষ চেকপয়েন্ট থেকে নতুন / আপডেট জিনিসগুলি চেক করি. কারণ ব্যান্ডবাইড মূল্যবান, এবং আমার ধৈর্যও তাই। //users.query-repository.ts async pull(dto: PullUsersDto): Promise<UserViewDto[]> { const { id, updatedAt, batchSize } = dto; const users = await this.users .createQueryBuilder('user') .where('user.updatedAt > :updatedAt', { updatedAt }) .orWhere('user.updatedAt = :updatedAt AND user.id > :id', { updatedAt, id, }) .orderBy('user.updatedAt', 'ASC') .addOrderBy('user.id', 'ASC') .limit(batchSize) .getMany(); return users.map(UserViewDto.mapToView); } এবং তারপর সার্ভার দুটি ডকুমেন্ট এবং একটি উজ্জ্বল নতুন চেকপয়েন্ট ফেরত পাঠায়: //user.service.ts async pull(dto: PullUsersDto) { const users = await this.usersRepository.pull(dto); const newCheckpoint = users.length === 0 ? { id: dto.id, updatedAt: dto.updatedAt } : { id: users.at(-1)!.id, updatedAt: users.at(-1)!.updatedAt, }; return { documents: users, checkpoint: newCheckpoint, }; } Step 5 — Pushing Local Changes Back to the Server ধাপ 5 - স্থানীয় পরিবর্তনগুলি সার্ভারে ফিরিয়ে আনুন আপনার অ্যাপ্লিকেশনের ডেটাগুলির জন্য ড্রপবক্সের মতো এটি চিন্তা করুন - কোনও রূপান্তরিত "সংঘাতযুক্ত কপি" ফাইল ছাড়া (হ্যাঁ ... যদি আপনি সংঘাতগুলি খারাপভাবে পরিচালনা না করেন তবে)। //instance.ts async handler(changeRows) { const response = await api.push({ changeRows }); return response.data; }, ব্যাক-ইন্ডে, আমি প্রত্যেকটি প্রবেশকারী পরিবর্তন চেক করি: যদি কোন সংঘাত না থাকে, তাহলে তা সরাসরি প্রবেশ করে। যদি সার্ভারের সংস্করণটি নতুন হয়, তবে আমি এটি সংঘর্ষের স্টাইলে ফেলে দেব। //user.service.ts async push(dto: PushUsersDto) { const changeRows = dto.changeRows; const existingUsers = await this.usersRepository.findByIds( changeRows.map((changeRow) => changeRow.newDocumentState.id), ); const existingMap = new Map(existingUsers.map((user) => [user.id, user])); const toSave: UserViewDto[] = []; const conflicts: UserViewDto[] = []; for (const changeRow of changeRows) { const newDoc = changeRow.newDocumentState; const existing = existingMap.get(newDoc.id); const isConflict = existing && existing.updatedAt > newDoc?.updatedAt; if (isConflict) { conflicts.push(existing); } else { toSave.push(newDoc); } if (toSave.length > 0) { await this.usersRepository.save(toSave); } } return conflicts; } Step 6 — Conflict Resolution (The “Last Update Wins” Gambit) ধাপ 6 – সংঘাত সমাধান (The “Last Update Wins” Gambit) এখানে মানুষ সাধারণত জিনিসগুলি অত্যন্ত জটিল করে। নাসা গ্রেড ফিউচার কৌশল তৈরি করুন. অথবা ... আপনি "শেষ আপডেট জিতে" এবং এটি একটি দিন বলা যেতে পারে। পারতেন এটা সবসময় সঠিক পদক্ষেপ নয়, কিন্তু আমাদের ক্ষেত্রে - সহজ, দ্রুত, যথেষ্ট ভাল। //utills.ts export const defaultConflictHandler: RxConflictHandler<{ updatedAt: number; }> = { isEqual(a, b) { return a.updatedAt === b.updatedAt; }, resolve({ assumedMasterState, realMasterState, newDocumentState }) { return Promise.resolve(realMasterState); }, }; Step 7 — Keeping the UI in Sync ধাপ 7 - UI সিনাক্ত রাখা init এর পরে, অ্যাপ স্ট্যাটাস ফ্লিপ , এবং আমরা শুধু ... এটি ব্যবহার করুন. কোন অদ্ভুত ম্যানুয়াল আপলোড বোতাম, কোন "পুনরায় লোড" বোকামি। Ready //UsersScreen.tsx export const UsersScreen = () => { const users = useUsersSelector({ sort: [{ updatedAt: 'desc' }], }); const { createUser, deleteUser, updateUser } = useUsersService(); return ( <View style={styles.container}> {users.map(user => ( <Text key={user.id}>{user.name}</Text> ))} <Button title={'Create new user'} onPress={createUser} /> <Button disabled={users.length === 0} title={'Update user'} onPress={() => updateUser(users[0].id)} /> <Button disabled={users.length === 0} title={'Delete user'} onPress={() => deleteUser(users[0].id)} /> </View> ); }; ডিবিতে পরিবর্তনগুলির জন্য সাবস্ক্রিপশন করে, তাই ইউআই নিজেকে আপডেট করে. এটি সেই "ম্যাজিকের মতো কাজ" মুহূর্তগুলির মধ্যে একটি যা আপনি খুব কঠোরভাবে চিন্তা করবেন না - এটি শুধু উপভোগ করুন। useUsersSelector //user.selector.tsx export const useUsersSelector = (query?: MangoQuery<UserType>) => { const userModel = RxDatabaseManager.getInstance().getDb().users; const [users, setUsers] = useState<UserType[]>([]); useEffect(() => { const subscription = userModel.find(query).$.subscribe(result => { setUsers(result); }); return () => subscription.unsubscribe(); }, [userModel, query]); return users; }; Final Thoughts চূড়ান্ত চিন্তা আমরা মূলত একটি অ্যাপ তৈরি করেছি যা: Offline কাজ করে। স্বয়ংক্রিয়ভাবে সমন্বয় আপনার Wi-Fi একটি অস্তিত্বগত সংকট আছে কিনা তা গুরুত্বপূর্ণ নয়। আমরা একই পদ্ধতি ব্যবহার করেছি। , যেখানে সংযোগযোগ ... শুধু বলুন "উরান্ডার অ্যাভেটিভ মোড" রাইডাররা এখন অর্ডারগুলি সম্পন্ন করতে পারে, প্রমাণিত ডেলিভারি ফটো নিতে পারে এবং ইন্টারনেট ছাড়াই ডেলিভারিগুলি সম্পন্ন করতে পারে। Sizl’s dark kitchen riders in Chicago অবশ্যই, বাস্তব জগতের ক্ষেত্রে আরও কঠিন হতে পারে - একই রেকর্ড আপডেট করে একাধিক ডিভাইস, সম্পর্কিত ডেটা মুছে ফেলা, বিশাল ডেটা সেট। আমি অবশ্যই স্থানীয়-প্রথম যেতে সুপারিশ করব? - যদি আপনি ব্যবহারকারীর রাগ টিকিট উপভোগ না করেন যা "আমি অফলাইন ছিলাম এবং ..." দিয়ে শুরু করে। Click the link in my bio for more info on this one & other projects! To check out our open source admin panel on React see our . Github গিটু