आप उस पल को जानते हैं जब आप कहीं भी के बीच में हैं, आपका 4G "ई" में बदल जाता है, और आपका मोबाइल ऐप एक बहुत महंगा कागज वजन बनने का फैसला करता है? एक डिलीवरी ड्राइवर को एक लोड स्पिनर पर नज़र डालने की कल्पना करें क्योंकि ऐप इंटरनेट के बिना अपना रास्ता लोड नहीं कर सकता है. या एक गोदाम प्रबंधक भंडारण सूची को खींचने में सक्षम नहीं है क्योंकि सर्वर नीचे है. यह "कुर्की व्यवहार" नहीं है, जो समय बर्बाद है, पैसा खो दिया गया है, और बहुत गुस्सा उपयोगकर्ता। हमारे कई ग्राहकों ने इन कनेक्टिविटी गड़बड़ों को मार डाला - रिमोट नौकरी साइटें, ऑफसेट घटनाएं, भंडारण मंजिल, रसद हब। मूल रूप से, जहां भी वाई-फाई मर जाता है। ठीक? स्थानीय-पहले ऐप्स: डेटा को स्थानीय रूप से संसाधित करें, जब भी आप कर सकते हैं, इसे सिंक्रनाइज़ करें। इस छोटे से साहसिक में, मैं आपको दिखाऊंगा कि मैंने एक स्थानीय-पहले मोबाइल ऐप कैसे बनाया आप देखेंगे: React Native + RxDB कैसे दो दिशात्मक सिंक्रनाइज़ वास्तव में आपके सर्वर को पिघलने के बिना काम करता है डेटा संघर्षों जैसे "बहुत मजेदार" स्थितियों को कैसे संभालें। इन जानवरों में से एक को डिजाइन करते समय क्या नहीं भूलना चाहिए। इसके अलावा - मैं एक "अंतिम अपडेट जीतता है" चाल साझा करूंगा. हमेशा सही विकल्प नहीं है, लेकिन हमारे मामले में ... शेफ का चुंबन। The Stack (a.k.a. My Chosen Weapons) मेरे चुने हुए हथियार (My Chosen Weapons) इस निर्माण के लिए, मैंने रोल किया: React Native - क्रॉस-प्लेटफॉर्म जादू: एक कोडबेस, आईओएस + एंड्रॉइड। react-native-nitro-sqlite - क्योंकि SQLite के बिना स्थानीय भंडारण प्याज के बिना प्याज की तरह है। RxDB — ऑफ़लाइन-पहले, प्रतिक्रियात्मक डीबी जो सिंक्रनाइज़ के साथ अच्छा खेलता है। 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(); } } इस छोटे से जानवर: DB को ऊपर रखें। एक क्लिंकी पूर्व की तरह इंटरनेट के लिए घड़ी। सिंक्रनाइज़ करें जैसे ही हम ऑनलाइन हैं। और हाँ, यह सब रिकॉर्ड करता है. भविष्य मैं अगली बार डिबग करते समय आभारी रहूंगा। 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 एक चेकपॉइंट भेजता है ( , ) इसलिए सर्वर जानता है कि हम कहां रुक गए हैं. क्योंकि कोई भी इसे उठाना नहीं चाहता है DB हर बार। 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 - स्थानीय परिवर्तनों को सर्वर पर वापस दबाएं RxDB भी ट्रैक करता है कि आखिरी सिंक्रनाइज़ के बाद से स्थानीय रूप से क्या बदल गया है और इसे बढ़ाता है. इसे Dropbox के रूप में अपने ऐप के डेटा के लिए सोचें - यादृच्छिक "विरोधी कॉपी" फ़ाइलों के बिना (ठीक है ... जब तक आप संघर्षों को खराब तरीके से संभाल नहीं करते हैं)। //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 - संघर्ष समाधान (आखिरी अद्यतन जीतता है) यहां लोग आमतौर पर चीजों को बहुत जटिल करते हैं. हाँ, आप या ... आप "पिछले अपडेट जीतता है" के साथ जा सकते हैं और इसे एक दिन कह सकते हैं। कर सकते हैं यह हमेशा सही कदम नहीं है, लेकिन हमारे मामले में - सरल, त्वरित, पर्याप्त अच्छा। //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 - यूआई को सिंक्रनाइज़ में रखें init के बाद, app status flips to , और हम सिर्फ ... इसका उपयोग करते हैं. कोई अजीब मैन्युअल अद्यतन बटन नहीं, कोई "रिचार्ज करने के लिए टैप" बकवास नहीं। 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 काम करता है। स्वचालित रूप से सिंक करें। इससे कोई फर्क नहीं पड़ता कि आपका वाई-फाई एक अस्तित्व संकट में है। हमने एक ही सिस्टम का इस्तेमाल किया। , जहां कनेक्टिविटी है ... बस "शहरी साहसिक मोड" कहें। चालक अब ऑर्डर पूरा कर सकते हैं, डिलीवरी के साबित फोटो ले सकते हैं, और इंटरनेट के बिना डिलीवरी को पूरा कर सकते हैं। 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 गीता