Hiçbir yerin ortasında olduğunuz o anı biliyor musunuz, 4G’niz “E” haline gelir ve mobil uygulamanız çok pahalı bir kağıt ağırlığı haline gelmeye karar verir mi? Bir teslimat sürücüsünü, uygulamanın internet olmadan rotalarını yükleyemediği için bir yükleme dönerine baktığını hayal edin. ya da bir depolama yöneticisi, sunucunun düştüğü için stok listesini çekemeyebilir. Bu, zaman kaybı, para kaybı ve çok kızgın kullanıcılar değildir. Müşterilerimizin bir kısmı bu bağlantı deliklerine çarptı – uzaktan iş siteleri, yeraltı etkinlikleri, depolama katları, lojistik merkezleri. Temel olarak, her yerde Wi-Fi ölür. düzeltme? Yerel ilk uygulamalar: verileri yerel olarak işleyin, mümkün olduğunca senkronize edin. kullanıcılarınız mutlu kalın ve gece yarısı “app’nin kırık” çağrıları almazsınız. Bu küçük macerada, size yerel bir mobil uygulamayı nasıl oluşturduğumu göstereceğim. Sen göreceksin ki: React Native + RxDB İki yönlü senkronizasyon, sunucunuzu eritmeden nasıl çalışır? Veri çelişkileri gibi eğlenceli durumlarla nasıl başa çıkılır? Bu hayvanlardan birini tasarlarken unutmamak gerekenler. Ayrıca – bir “son güncelleme kazanır” hileyi paylaşacağım. her zaman doğru seçim değil, ama bizim durumumuzda... şefin öpücüğü. The Stack (a.k.a. My Chosen Weapons) The Stack (My Chosen Weapons - Benim Seçtiğim Silahlar) Bu yapıyı yaparken ben de çöktüm: React Native – platformlar arası sihir: bir kod tabanı, iOS + Android. react-native-nitro-sqlite - çünkü SQLite olmadan yerel depolama peynir olmadan pizza gibidir. RxDB - senkronizasyon ile güzel oynayan çevrimdışı ilk, reaksiyonel DB. NestJS + TypeORM + PostgreSQL – arka uç rüya ekibi. Sonuç: uygulama çevrimdışı çalışır, daha sonra senkronize olur ve bir bozulma olmadan gizli bağlantıları hayatta kalır. Step 1 — Local Storage via SQLite 1. Adım: SQLite üzerinden yerel depolama İlk olarak, RxDB'nin mutlu bir şekilde kötüye kullanabileceği bir yerel veritabanına ihtiyacım vardı. SQLite açık bir seçimdir, ancak verilerin bütünlüğü ve gizliliği önemli olduğu için (ve aynı zamanda gelecekteki kendime teşekkür edeceğim için) bazı ekstra güçlerle sarıldım. //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 }; Evet, bu üç katmanlı ambalaj. Bir soğan gibi. Ya da çok fazla middleware katmanı olan bir işletme uygulaması. Step 2 — Creating the Database Instance Adım 2: Database Instance oluşturma Sonrasında ise a inşa ettim Çünkü DB'nizin birden fazla örneğine sahip olmanın iyi bir fikir olduğunu düşünüyorsanız ... muhtemelen üretimde birleşme çatışmalarını da seveceksiniz. RxDatabaseManager İşte tüm yüceliğindeki sınıf: //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(); } } Bu küçük canavar: DB’yi ayarlayın Önceki İçerikİzmir’de bir internet gibi gözüküyor İnternetten döndüğümüzde sinirleniyoruz. Sonraki İçerikHayırlı Olsun, Gelecekte Şükretmek İsterim! Step 3 — Bootstrapping the DB When the App Starts Adım 3 - Uygulama Başlatıldığında DB'yi Bootstrapping Uygulamayı açtığımda, benim “Hiçbir sihir yoktur, sadece iyi ol” kendi işini yapıyor. RxDatabaseManager useEffect init sırasında bir şey patlarsa, bunu kaydederim. hataları var etmemek gibi davranmak, uygulamalardan nasıl korktuğunuzu gösterir. //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) Adım 4 – Veri Replikasyonu (a.k.a. Gözyaşları Olmadan Senkronizasyon) Uygulama “offline mağara modundan” çevrimiçi olarak döndüğünde, Bu, yerel DB ile sunucu arasında veri senkronizasyonunu devre dışı bırakır . onReconnected() replicateRxCollection RxDB kontrol noktası ( RxDB kontrol noktası) ve ) böylece sunucu nerede durduğumuzu biliyor. çünkü kimse geri almak istemiyor DB her seferinde. updatedAt id Tüm //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, }; }, Sunucu üzerinde, son kontrol noktasından beri sadece yeni / güncellenmiş şeyleri sorguluyorum.Çünkü bant genişliği değerlidir ve benim sabrım da öyle. //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); } Ve sonra sunucu hem dosyaları hem de parlak yeni bir kontrol noktası geri gönderir: //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. Adım: Yerel değişiklikleri sunucuya geri yükleyin RxDB, son senkronizasyonundan bu yana yerel olarak neyin değiştiğini izler ve Dropbox gibi uygulamanızın verileri için düşünün - rastgele “konflik kopyası” dosyaları olmadan (iyi... çatışmaları kötü bir şekilde ele almazsanız). //instance.ts async handler(changeRows) { const response = await api.push({ changeRows }); return response.data; }, Gelen değişiklikleri kontrol etmek için aşağıdaki adımları izleyin: Eğer çatışma yoksa doğrudan içeri girer. Server'ın versiyonu daha yeni ise, onu çatışma kümesine atarım. //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) Adım 6 – Çatışma Çözümü (The “Last Update Wins” Gambit) İşte insanlar genellikle aşırı karmaşık şeyler nerede. evet, sen Ya da “son güncelleme kazanır” ile gidebilir ve bunu bir gün olarak adlandırabilirsiniz. olabilir Her zaman doğru bir hareket değildir, ancak bizim durumumuzda - basit, hızlı, yeterince iyi. //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 Adım 7 – UI’yi Sync’de tutmak init'i yaptıktan sonra, app statüsü döner Hiçbir garip manuel yenileme düğmeleri, hiçbir “yine yükle” saçmalık. 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> ); }; DB'de değişiklikler için abonelik yapar, böylece kullanıcı arayüzü kendini güncelleştirir. Bu, çok fazla düşünmemesi gereken o “sihir gibi çalışır” anlardan biridir - sadece zevk alın. 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 Son Düşünceler Temel olarak, aşağıdakileri yapan bir uygulama oluşturduk: Offline çalışıyor Otomatik olarak sinirlenir. Wi-Fi’nizin varoluş krizine girdiğini düşünmeyin. Biz de aynı ayarları kullandık , bağlantılı olduğu yerde... sadece “şehir macerası modu” diyelim.Riderler şimdi siparişleri tamamlayabilir, teslimat kanıtı fotoğrafları çekebilir ve internet olmadan teslimatları işaretleyebilir. Sizl’s dark kitchen riders in Chicago Elbette, gerçek dünyadaki durumlar daha da kötüye gidebilir – aynı kayıtları güncelleyen çok sayıda cihaz, ilgili veri silinmesi, büyük veri kümeleri. Kesinlikle – “Ben çevrimdışıydım ve...” ile başlayan kullanıcı öfkesi biletlerini beğenmedikçe. 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 github