From 0eccfe5f1f18656061f3e5da9dffac40d262239d Mon Sep 17 00:00:00 2001 From: Nicholai Date: Tue, 24 Feb 2026 17:51:59 -0700 Subject: [PATCH] 2026-02-25T00-51-59_auto_memory/memories.db-wal --- .skill-lock.json | 9 + memory/memories.db-wal | Bin 4972872 -> 4972872 bytes skills/mobile-offline-support/SKILL.md | 466 +++++++++++++++++++++++++ 3 files changed, 475 insertions(+) create mode 100644 skills/mobile-offline-support/SKILL.md diff --git a/.skill-lock.json b/.skill-lock.json index 4102749c2..cbbc8fd4d 100644 --- a/.skill-lock.json +++ b/.skill-lock.json @@ -153,6 +153,15 @@ "skillFolderHash": "c5ce954ef1fca62dd177cd1fd16e2fc81898458a", "installedAt": "2026-02-25T00:51:33.360Z", "updatedAt": "2026-02-25T00:51:33.360Z" + }, + "mobile-offline-support": { + "source": "aj-geddes/useful-ai-prompts", + "sourceType": "github", + "sourceUrl": "https://github.com/aj-geddes/useful-ai-prompts.git", + "skillPath": "skills/mobile-offline-support/SKILL.md", + "skillFolderHash": "05bb4c1a9463fe22342c09422760c5526d2dac06", + "installedAt": "2026-02-25T00:51:58.400Z", + "updatedAt": "2026-02-25T00:51:58.400Z" } }, "dismissed": { diff --git a/memory/memories.db-wal b/memory/memories.db-wal index ff1e848ec00087219493147d2ad81897dab94fa2..fd67c79fa6fa87231fff2b6767d6790e977672fa 100644 GIT binary patch delta 1332 zcmYk*4@^{b902h5?jOIqd+*-yzvG1Czan_?zk?1q{sae!2LuU-i-?d!3#?f(QqbQ` zr7W!GTag305L}7C<^3GcWFm_~8aUd-O(MxmhJ=f1$p(FI%3Z&=&u{zfegFJ^`@J`I z^`c-5G-4VHun>!|7)!7VMp%kvSdJOYVg=@~61!qI?2bLKC-%bL_^EGHUxe_9?g9n6 z>TgZ?d@A{4zTM3`h#u=JTjWq$^rH`H)!YJU-0e3ahV*_xf zbNvFT{Y}yDe4A%owHC4HkxtuZn0Z}J4By%}QKX*pWoqXeNV|mVM~+fPYxu1q7bGmy zmQaKZ zZ5eEsGsQx3O1Lx5Xbb5Gkft!GR)H;<1E~wPXc|tM)p2eqK6}iEpR7qz3v;Pl-Y;1i zM%F`A*SN>`<8oVF)j8wVPRfsS=UmW}E3G24C3FSalKx5C#D0GBEX$=)A}W>Q-lZN= zsq|Utigcyb*FHaf_^3)cfTX^k^uv>~r)TqS-_4Q~l9n#+47*X*K-P}X2L$NU`RcF> zpZzT-<%gxP%Rm3UpY48h43e3CHMOqeZ#n5Dp^1HlPJhgGdksqCpIZ1#uuA rJOgwf0VINFfgU7*WRL<3AQhy6bdUj_1DPNT7(q73A%nW-Pj&wXE&JOx delta 1393 zcmYk(eNa?Y6aet|-oAzFvTygHg1T?leNR4Bz$I{9Sr!(NLZbQm4pFrqdh#%f`3 ztnD(Cm+gF<{cU8bee&s%R|Q-PU=K0cQr@NExLPibJ;XZI=b6&iFmH(|!zjE6f8*Yj zlv9sRA3l~R?F%Hfgg8e+LjDXm?2!rxNjWmSaE5hz26%Q`MKRhxcUH%D;bpag!OH|N zos6-!2Qc(TNZ|gS?9b$>S-HU_enbIPL@ieJQZ~R;}JxWp_ZL@0*3l-}m zx1M86YVaq9P`JL>RLBlz80)%Mi&=20K5^n+TQyXEZ><&vo)-&o=dG(aGJRNS!`@WI z0-o)XD3UvWE_M8M=4TzWT*udvuhLudswTa=1uBnAbN)X&7?Ph^>o{`mV4KK4*-!)B z|MUe%#Q)~hs%JMt{q+7%SWf}9gRpVLi9>TkLLUo4BsV4*ol-lQcf z3!d#O%6&cra^12hnlx)yXI)xaIZ!Q;%XBmw#L}Pqx3^7iDihT2JgX$d}9z5zT z9nX|R_azM&;`1+Oj5eKyDSR;}K7SFzhitv?Qn>Y0y|LI-V!(~jGhgg#ef|}yxHQ>8 z4#v+ed@!YF1^8>^>l*c!YH1Lsb;H>`a-@E!zBQ-mRxcCT*8+}rGW;Fh2~8iG6L_^Y z$z2DZe`JmbB4zUz1>9-d&;a6zzCu^}P8;`^`C7rh!;I5n*MXc(=Y!M1eM8P-`xDq9 zr_S7HAes5)^&$SAX&^TC#csQGB5l9TI|7PWrG)E`jfbZig$jgr)pb?AIa31nHFG_u zc2rA&oHhk|e>Kk%$i>wkd&W*(SPV6af*1Xe-81izd3gvF^_uHRf2p6 zvn#&`^uUEQC2^o+I~X&Ri+U2_co_3{O7{||ku1prS;j-`Oy!(FGQG!dez{^>*RcY{ zGf>KSShi5vYaq!{KgtC=ujRo7r{!w@LFTBMTI_Cu_fjpD?0>IdPD1Wl<+6^fYME~D zbiRK8Y^6$6f2~Xvn5J2(2STfWEmKnU8`~WcG9wEbiL6LQ3L1sNP&kS}kthm9qtR## z8jE63EQ&+pkPXG7*H8jVMB|YiO+XGb5hWofN=7b}f>MzirJ+eE9Zg0V=yfy&WumDl L3)&so-u?dqjKKDl diff --git a/skills/mobile-offline-support/SKILL.md b/skills/mobile-offline-support/SKILL.md new file mode 100644 index 000000000..0074c06e8 --- /dev/null +++ b/skills/mobile-offline-support/SKILL.md @@ -0,0 +1,466 @@ +--- +name: mobile-offline-support +description: Implement offline-first mobile apps with local storage, sync strategies, and conflict resolution. Covers AsyncStorage, Realm, SQLite, and background sync patterns. +--- + +# Mobile Offline Support + +## Overview + +Design offline-first mobile applications that provide seamless user experience regardless of connectivity. + +## When to Use + +- Building apps that work without internet connection +- Implementing seamless sync when connectivity returns +- Handling data conflicts between device and server +- Reducing server load with intelligent caching +- Improving app responsiveness with local storage + +## Instructions + +### 1. **React Native Offline Storage** + +```javascript +import AsyncStorage from '@react-native-async-storage/async-storage'; +import NetInfo from '@react-native-community/netinfo'; + +class StorageManager { + static async saveItems(items) { + try { + await AsyncStorage.setItem( + 'items_cache', + JSON.stringify({ data: items, timestamp: Date.now() }) + ); + } catch (error) { + console.error('Failed to save items:', error); + } + } + + static async getItems() { + try { + const data = await AsyncStorage.getItem('items_cache'); + return data ? JSON.parse(data) : null; + } catch (error) { + console.error('Failed to retrieve items:', error); + return null; + } + } + + static async queueAction(action) { + try { + const queue = await AsyncStorage.getItem('action_queue'); + const actions = queue ? JSON.parse(queue) : []; + actions.push({ ...action, id: Date.now(), attempts: 0 }); + await AsyncStorage.setItem('action_queue', JSON.stringify(actions)); + } catch (error) { + console.error('Failed to queue action:', error); + } + } + + static async getActionQueue() { + try { + const queue = await AsyncStorage.getItem('action_queue'); + return queue ? JSON.parse(queue) : []; + } catch (error) { + return []; + } + } + + static async removeFromQueue(actionId) { + try { + const queue = await AsyncStorage.getItem('action_queue'); + const actions = queue ? JSON.parse(queue) : []; + const filtered = actions.filter(a => a.id !== actionId); + await AsyncStorage.setItem('action_queue', JSON.stringify(filtered)); + } catch (error) { + console.error('Failed to remove from queue:', error); + } + } +} + +class OfflineAPIService { + async fetchItems() { + const isOnline = await this.checkConnectivity(); + + if (isOnline) { + try { + const response = await fetch('https://api.example.com/items'); + const items = await response.json(); + await StorageManager.saveItems(items); + return items; + } catch (error) { + const cached = await StorageManager.getItems(); + return cached?.data || []; + } + } else { + const cached = await StorageManager.getItems(); + return cached?.data || []; + } + } + + async createItem(item) { + const isOnline = await this.checkConnectivity(); + + if (isOnline) { + try { + const response = await fetch('https://api.example.com/items', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(item) + }); + const created = await response.json(); + return { success: true, data: created }; + } catch (error) { + await StorageManager.queueAction({ + type: 'CREATE_ITEM', + payload: item + }); + return { success: false, queued: true }; + } + } else { + await StorageManager.queueAction({ + type: 'CREATE_ITEM', + payload: item + }); + return { success: false, queued: true }; + } + } + + async syncQueue() { + const queue = await StorageManager.getActionQueue(); + + for (const action of queue) { + try { + await this.executeAction(action); + await StorageManager.removeFromQueue(action.id); + } catch (error) { + action.attempts = (action.attempts || 0) + 1; + if (action.attempts > 3) { + await StorageManager.removeFromQueue(action.id); + } + } + } + } + + private async executeAction(action) { + switch (action.type) { + case 'CREATE_ITEM': + return fetch('https://api.example.com/items', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(action.payload) + }); + default: + return Promise.reject(new Error('Unknown action type')); + } + } + + async checkConnectivity() { + const state = await NetInfo.fetch(); + return state.isConnected ?? false; + } +} + +export function OfflineListScreen() { + const [items, setItems] = useState([]); + const [isOnline, setIsOnline] = useState(true); + const [syncing, setSyncing] = useState(false); + const apiService = new OfflineAPIService(); + + useFocusEffect( + useCallback(() => { + loadItems(); + const unsubscribe = NetInfo.addEventListener(state => { + setIsOnline(state.isConnected ?? false); + if (state.isConnected) { + syncQueue(); + } + }); + + return unsubscribe; + }, []) + ); + + const loadItems = async () => { + const items = await apiService.fetchItems(); + setItems(items); + }; + + const syncQueue = async () => { + setSyncing(true); + await apiService.syncQueue(); + await loadItems(); + setSyncing(false); + }; + + return ( + + {!isOnline && Offline Mode} + {syncing && } + } + keyExtractor={item => item.id} + /> + + ); +} +``` + +### 2. **iOS Core Data Implementation** + +```swift +import CoreData + +class PersistenceController { + static let shared = PersistenceController() + + let container: NSPersistentContainer + + init(inMemory: Bool = false) { + container = NSPersistentContainer(name: "MyApp") + + if inMemory { + container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null") + } + + container.loadPersistentStores { _, error in + if let error = error as NSError? { + print("Core Data load error: \(error)") + } + } + + container.viewContext.automaticallyMergesChangesFromParent = true + } + + func save(_ context: NSManagedObjectContext = PersistenceController.shared.container.viewContext) { + if context.hasChanges { + do { + try context.save() + } catch { + print("Save error: \(error)") + } + } + } +} + +// Core Data Models +@NSManaged class ItemEntity: NSManagedObject { + @NSManaged var id: String + @NSManaged var title: String + @NSManaged var description: String? + @NSManaged var isSynced: Bool +} + +@NSManaged class ActionQueueEntity: NSManagedObject { + @NSManaged var id: UUID + @NSManaged var type: String + @NSManaged var payload: Data? + @NSManaged var createdAt: Date +} + +class OfflineSyncManager: NSObject, ObservableObject { + @Published var isOnline = true + @Published var isSyncing = false + + private let networkMonitor = NWPathMonitor() + private let persistenceController = PersistenceController.shared + + override init() { + super.init() + setupNetworkMonitoring() + } + + private func setupNetworkMonitoring() { + networkMonitor.pathUpdateHandler = { [weak self] path in + DispatchQueue.main.async { + self?.isOnline = path.status == .satisfied + if path.status == .satisfied { + self?.syncWithServer() + } + } + } + + let queue = DispatchQueue(label: "NetworkMonitor") + networkMonitor.start(queue: queue) + } + + func saveItem(_ item: Item) { + let context = persistenceController.container.viewContext + let entity = ItemEntity(context: context) + entity.id = item.id + entity.title = item.title + entity.isSynced = false + + persistenceController.save(context) + + if isOnline { + syncItem(item) + } + } + + func syncWithServer() { + isSyncing = true + let context = persistenceController.container.viewContext + let request: NSFetchRequest = ActionQueueEntity.fetchRequest() + + do { + let pendingActions = try context.fetch(request) + for action in pendingActions { + context.delete(action) + } + persistenceController.save(context) + } catch { + print("Sync error: \(error)") + } + + isSyncing = false + } +} +``` + +### 3. **Android Room Database** + +```kotlin +@Entity(tableName = "items") +data class ItemEntity( + @PrimaryKey val id: String, + val title: String, + val description: String?, + val isSynced: Boolean = false +) + +@Entity(tableName = "action_queue") +data class ActionQueueEntity( + @PrimaryKey val id: Long = System.currentTimeMillis(), + val type: String, + val payload: String, + val createdAt: Long = System.currentTimeMillis() +) + +@Dao +interface ItemDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertItem(item: ItemEntity) + + @Query("SELECT * FROM items") + fun getAllItems(): Flow> + + @Update + suspend fun updateItem(item: ItemEntity) +} + +@Dao +interface ActionQueueDao { + @Insert + suspend fun insertAction(action: ActionQueueEntity) + + @Query("SELECT * FROM action_queue ORDER BY createdAt ASC") + suspend fun getAllActions(): List + + @Delete + suspend fun deleteAction(action: ActionQueueEntity) +} + +@Database(entities = [ItemEntity::class, ActionQueueEntity::class], version = 1) +abstract class AppDatabase : RoomDatabase() { + abstract fun itemDao(): ItemDao + abstract fun actionQueueDao(): ActionQueueDao +} + +@HiltViewModel +class OfflineItemsViewModel @Inject constructor( + private val itemDao: ItemDao, + private val actionQueueDao: ActionQueueDao, + private val connectivityManager: ConnectivityManager +) : ViewModel() { + private val _items = MutableStateFlow>(emptyList()) + val items: StateFlow> = _items.asStateFlow() + + init { + viewModelScope.launch { + itemDao.getAllItems().collect { entities -> + _items.value = entities.map { it.toItem() } + } + } + observeNetworkConnectivity() + } + + fun saveItem(item: Item) { + viewModelScope.launch { + val entity = item.toEntity() + itemDao.insertItem(entity) + + if (isNetworkAvailable()) { + syncItem(item) + } else { + actionQueueDao.insertAction( + ActionQueueEntity( + type = "CREATE_ITEM", + payload = Json.encodeToString(item) + ) + ) + } + } + } + + private fun observeNetworkConnectivity() { + val networkRequest = NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .build() + + connectivityManager.registerNetworkCallback( + networkRequest, + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + viewModelScope.launch { syncQueue() } + } + } + ) + } + + private suspend fun syncQueue() { + val queue = actionQueueDao.getAllActions() + for (action in queue) { + try { + actionQueueDao.deleteAction(action) + } catch (e: Exception) { + println("Sync error: ${e.message}") + } + } + } + + private fun isNetworkAvailable(): Boolean { + val activeNetwork = connectivityManager.activeNetwork ?: return false + val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false + return capabilities.hasCapability(NET_CAPABILITY_INTERNET) + } +} +``` + +## Best Practices + +### ✅ DO +- Implement robust local storage +- Use automatic sync when online +- Provide visual feedback for offline status +- Queue actions for later sync +- Handle conflicts gracefully +- Cache frequently accessed data +- Implement proper error recovery +- Test offline scenarios thoroughly +- Use compression for large data +- Monitor storage usage + +### ❌ DON'T +- Assume constant connectivity +- Sync large files frequently +- Ignore storage limitations +- Force unnecessary syncing +- Lose data on offline mode +- Store sensitive data unencrypted +- Accumulate infinite queue items +- Ignore sync failures silently +- Sync in tight loops +- Deploy without offline testing