From 3cf10b1db42b3644d52f772e7cd7f4bf39231a89 Mon Sep 17 00:00:00 2001 From: padreug Date: Sat, 6 Sep 2025 12:08:39 +0200 Subject: [PATCH] 1.3.4 User-Scoped Storage Pattern: Add StorageService integration across modules for improved data management - Introduced STORAGE_SERVICE token in the DI container for consistent service registration. - Updated BaseService to include storageService as a dependency, ensuring proper initialization and error handling. - Refactored ChatService to utilize storageService for managing unread messages and peers, replacing localStorage usage. - Enhanced MarketStore to save and load orders using storageService, improving data persistence and user experience. - Registered storageService in the base module, ensuring it is initialized and disposed of correctly. This integration streamlines data handling across the application, promoting better maintainability and consistency. --- src/core/base/BaseService.ts | 7 + src/core/di-container.ts | 3 + src/core/services/StorageService.ts | 202 ++++++++++++++++++++++ src/modules/base/index.ts | 11 ++ src/modules/chat/services/chat-service.ts | 70 +++----- src/modules/market/stores/market.ts | 95 ++++------ 6 files changed, 285 insertions(+), 103 deletions(-) create mode 100644 src/core/services/StorageService.ts diff --git a/src/core/base/BaseService.ts b/src/core/base/BaseService.ts index 82016b8..e6a9dd9 100644 --- a/src/core/base/BaseService.ts +++ b/src/core/base/BaseService.ts @@ -45,6 +45,7 @@ export abstract class BaseService { protected relayHub: any = null protected authService: any = null protected visibilityService: any = null + protected storageService: any = null // Service state public readonly isInitialized: Ref = ref(false) @@ -134,6 +135,7 @@ export abstract class BaseService { this.relayHub = tryInjectService(SERVICE_TOKENS.RELAY_HUB) this.authService = tryInjectService(SERVICE_TOKENS.AUTH_SERVICE) this.visibilityService = tryInjectService(SERVICE_TOKENS.VISIBILITY_SERVICE) + this.storageService = tryInjectService(SERVICE_TOKENS.STORAGE_SERVICE) // Check if all required dependencies are available const missingDeps = this.getMissingDependencies() @@ -181,6 +183,9 @@ export abstract class BaseService { if (deps.includes('VisibilityService') && !this.visibilityService) { missing.push('VisibilityService') } + if (deps.includes('StorageService') && !this.storageService) { + missing.push('StorageService') + } return missing } @@ -264,6 +269,8 @@ export abstract class BaseService { this.isInitialized.value = false this.relayHub = null this.authService = null + this.visibilityService = null + this.storageService = null console.log(`♻️ ${this.metadata.name} disposed`) diff --git a/src/core/di-container.ts b/src/core/di-container.ts index d3be71c..9035af7 100644 --- a/src/core/di-container.ts +++ b/src/core/di-container.ts @@ -119,6 +119,9 @@ export const SERVICE_TOKENS = { // Visibility services VISIBILITY_SERVICE: Symbol('visibilityService'), + // Storage services + STORAGE_SERVICE: Symbol('storageService'), + // Market services MARKET_STORE: Symbol('marketStore'), PAYMENT_MONITOR: Symbol('paymentMonitor'), diff --git a/src/core/services/StorageService.ts b/src/core/services/StorageService.ts new file mode 100644 index 0000000..a438ce6 --- /dev/null +++ b/src/core/services/StorageService.ts @@ -0,0 +1,202 @@ +import { BaseService } from '@/core/base/BaseService' + +/** + * Centralized storage service providing user-scoped localStorage operations + * + * Features: + * - Automatic user-scoped key generation + * - Type-safe storage operations + * - Consistent data isolation between users + * - Fallback to global storage for anonymous users + * - Error handling with graceful degradation + */ +export class StorageService extends BaseService { + // Service metadata + protected readonly metadata = { + name: 'StorageService', + version: '1.0.0', + dependencies: ['AuthService'] // Depends on auth for user pubkey + } + + /** + * Service-specific initialization (called by BaseService) + */ + protected async onInitialize(): Promise { + this.debug('StorageService initialized') + } + + /** + * Generate user-scoped storage key + * @param baseKey - Base key to scope to current user + * @returns User-scoped key or base key if no user + */ + private getUserStorageKey(baseKey: string): string { + if (!this.authService?.user?.value?.pubkey) { + // No user authenticated, use global key + return baseKey + } + + const userPubkey = this.authService.user.value.pubkey + return `${baseKey}_${userPubkey}` + } + + /** + * Store user-scoped data in localStorage + * @param key - Base storage key + * @param data - Data to store (will be JSON serialized) + */ + setUserData(key: string, data: T): void { + try { + const storageKey = this.getUserStorageKey(key) + const serializedData = JSON.stringify(data) + localStorage.setItem(storageKey, serializedData) + + this.debug(`Stored user data: ${storageKey}`) + } catch (error) { + console.error(`Failed to store user data for key "${key}":`, error) + } + } + + /** + * Retrieve user-scoped data from localStorage + * @param key - Base storage key + * @param defaultValue - Default value if not found + * @returns Stored data or default value + */ + getUserData(key: string, defaultValue?: T): T | undefined { + try { + const storageKey = this.getUserStorageKey(key) + const stored = localStorage.getItem(storageKey) + + if (stored === null) { + this.debug(`No stored data found for key: ${storageKey}`) + return defaultValue + } + + const parsed = JSON.parse(stored) as T + this.debug(`Retrieved user data: ${storageKey}`) + return parsed + + } catch (error) { + console.error(`Failed to retrieve user data for key "${key}":`, error) + return defaultValue + } + } + + /** + * Remove user-scoped data from localStorage + * @param key - Base storage key + */ + clearUserData(key: string): void { + try { + const storageKey = this.getUserStorageKey(key) + localStorage.removeItem(storageKey) + + this.debug(`Cleared user data: ${storageKey}`) + } catch (error) { + console.error(`Failed to clear user data for key "${key}":`, error) + } + } + + /** + * Check if user-scoped data exists + * @param key - Base storage key + * @returns True if data exists + */ + hasUserData(key: string): boolean { + try { + const storageKey = this.getUserStorageKey(key) + return localStorage.getItem(storageKey) !== null + } catch (error) { + console.error(`Failed to check user data for key "${key}":`, error) + return false + } + } + + /** + * Get all keys for the current user (useful for cleanup) + * @returns Array of storage keys belonging to current user + */ + getUserKeys(): string[] { + try { + if (!this.authService?.user?.value?.pubkey) { + return [] + } + + const userPubkey = this.authService.user.value.pubkey + const userKeys: string[] = [] + + // Scan localStorage for keys belonging to this user + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) + if (key && key.endsWith(`_${userPubkey}`)) { + userKeys.push(key) + } + } + + return userKeys + } catch (error) { + console.error('Failed to get user keys:', error) + return [] + } + } + + /** + * Clear all data for the current user (useful on logout) + */ + clearAllUserData(): void { + try { + const userKeys = this.getUserKeys() + + for (const key of userKeys) { + localStorage.removeItem(key) + } + + this.debug(`Cleared all user data: ${userKeys.length} keys`) + } catch (error) { + console.error('Failed to clear all user data:', error) + } + } + + /** + * Migrate data from old storage pattern to user-scoped pattern + * @param oldKey - Old global key + * @param newKey - New base key for user scoping + */ + migrateToUserScoped(oldKey: string, newKey: string): void { + try { + // Only migrate if we have a user and old data exists + if (!this.authService?.user?.value?.pubkey) { + return + } + + const oldData = localStorage.getItem(oldKey) + if (oldData) { + const parsed = JSON.parse(oldData) as T + this.setUserData(newKey, parsed) + localStorage.removeItem(oldKey) + + this.debug(`Migrated data from "${oldKey}" to user-scoped "${newKey}"`) + } + } catch (error) { + console.error(`Failed to migrate data from "${oldKey}" to "${newKey}":`, error) + } + } + + /** + * Get current user pubkey (for debugging) + */ + getCurrentUserPubkey(): string | null { + return this.authService?.user?.value?.pubkey || null + } + + /** + * Cleanup when service is disposed (called by BaseService) + */ + protected async onDispose(): Promise { + this.debug('StorageService disposed') + } +} + +// Export singleton instance +export const storageService = new StorageService() \ No newline at end of file diff --git a/src/modules/base/index.ts b/src/modules/base/index.ts index ddcfc02..a102f4d 100644 --- a/src/modules/base/index.ts +++ b/src/modules/base/index.ts @@ -12,6 +12,7 @@ import { pwaService } from './pwa/pwa-service' // Import core services import { paymentService } from '@/core/services/PaymentService' import { visibilityService } from '@/core/services/VisibilityService' +import { storageService } from '@/core/services/StorageService' /** * Base Module Plugin @@ -36,6 +37,9 @@ export const baseModule: ModulePlugin = { // Register visibility service container.provide(SERVICE_TOKENS.VISIBILITY_SERVICE, visibilityService) + // Register storage service + container.provide(SERVICE_TOKENS.STORAGE_SERVICE, storageService) + // Register PWA service container.provide('pwaService', pwaService) @@ -54,6 +58,10 @@ export const baseModule: ModulePlugin = { waitForDependencies: false, // VisibilityService has no dependencies maxRetries: 1 }) + await storageService.initialize({ + waitForDependencies: true, // StorageService depends on AuthService + maxRetries: 3 + }) console.log('✅ Base module installed successfully') }, @@ -66,6 +74,7 @@ export const baseModule: ModulePlugin = { await auth.dispose() await paymentService.dispose() await visibilityService.dispose() + await storageService.dispose() console.log('✅ Base module uninstalled') }, @@ -74,6 +83,8 @@ export const baseModule: ModulePlugin = { relayHub, auth, paymentService, + visibilityService, + storageService, pwaService }, diff --git a/src/modules/chat/services/chat-service.ts b/src/modules/chat/services/chat-service.ts index 7c7bf71..0aa52de 100644 --- a/src/modules/chat/services/chat-service.ts +++ b/src/modules/chat/services/chat-service.ts @@ -6,15 +6,13 @@ import type { ChatMessage, ChatPeer, UnreadMessageData, ChatConfig } from '../ty import { getAuthToken } from '@/lib/config/lnbits' import { config } from '@/lib/config' -const UNREAD_MESSAGES_KEY = 'nostr-chat-unread-messages' -const PEERS_KEY = 'nostr-chat-peers' export class ChatService extends BaseService { // Service metadata protected readonly metadata = { name: 'ChatService', version: '1.0.0', - dependencies: ['RelayHub', 'AuthService', 'VisibilityService'] + dependencies: ['RelayHub', 'AuthService', 'VisibilityService', 'StorageService'] } // Service-specific state @@ -64,6 +62,9 @@ export class ChatService extends BaseService { * Complete the initialization once all dependencies are available */ private async completeInitialization(): Promise { + // Load peers from storage first + this.loadPeersFromStorage() + // Load peers from API await this.loadPeersFromAPI().catch(error => { console.warn('Failed to load peers from API:', error) @@ -290,36 +291,24 @@ export class ChatService extends BaseService { } private getUnreadData(peerPubkey: string): UnreadMessageData { - try { - const stored = localStorage.getItem(`${UNREAD_MESSAGES_KEY}-${peerPubkey}`) - if (stored) { - const data = JSON.parse(stored) - return { - ...data, - processedMessageIds: new Set(data.processedMessageIds || []) - } - } - } catch (error) { - console.warn('Failed to load unread data for peer:', peerPubkey, error) - } + const data = this.storageService.getUserData(`chat-unread-messages-${peerPubkey}`, { + lastReadTimestamp: 0, + unreadCount: 0, + processedMessageIds: [] + }) - return { - lastReadTimestamp: 0, - unreadCount: 0, - processedMessageIds: new Set() + return { + ...data, + processedMessageIds: new Set(data.processedMessageIds || []) } } private saveUnreadData(peerPubkey: string, data: UnreadMessageData): void { - try { - const serializable = { - ...data, - processedMessageIds: Array.from(data.processedMessageIds) - } - localStorage.setItem(`${UNREAD_MESSAGES_KEY}-${peerPubkey}`, JSON.stringify(serializable)) - } catch (error) { - console.warn('Failed to save unread data for peer:', peerPubkey, error) + const serializable = { + ...data, + processedMessageIds: Array.from(data.processedMessageIds) } + this.storageService.setUserData(`chat-unread-messages-${peerPubkey}`, serializable) } // Load peers from API @@ -369,26 +358,21 @@ export class ChatService extends BaseService { } private loadPeersFromStorage(): void { - try { - const stored = localStorage.getItem(PEERS_KEY) - if (stored) { - const peersArray = JSON.parse(stored) as ChatPeer[] - peersArray.forEach(peer => { - this.peers.value.set(peer.pubkey, peer) - }) - } - } catch (error) { - console.warn('Failed to load peers from storage:', error) + // Skip loading peers in constructor as StorageService may not be available yet + // This will be called later during initialization when dependencies are ready + if (!this.isInitialized.value) { + return } + + const peersArray = this.storageService.getUserData('chat-peers', []) as ChatPeer[] + peersArray.forEach(peer => { + this.peers.value.set(peer.pubkey, peer) + }) } private savePeersToStorage(): void { - try { - const peersArray = Array.from(this.peers.value.values()) - localStorage.setItem(PEERS_KEY, JSON.stringify(peersArray)) - } catch (error) { - console.warn('Failed to save peers to storage:', error) - } + const peersArray = Array.from(this.peers.value.values()) + this.storageService.setUserData('chat-peers', peersArray) } // Load message history for known peers diff --git a/src/modules/market/stores/market.ts b/src/modules/market/stores/market.ts index 4426339..04c38d2 100644 --- a/src/modules/market/stores/market.ts +++ b/src/modules/market/stores/market.ts @@ -4,6 +4,7 @@ import { invoiceService } from '@/lib/services/invoiceService' import { paymentMonitor } from '@/lib/services/paymentMonitor' import { nostrmarketService } from '../services/nostrmarketService' import { useAuth } from '@/composables/useAuth' +import { injectService, SERVICE_TOKENS } from '@/core/di-container' import type { LightningInvoice } from '@/lib/services/invoiceService' @@ -16,12 +17,7 @@ import type { export const useMarketStore = defineStore('market', () => { const auth = useAuth() - - // Helper function to get user-specific storage key - const getUserStorageKey = (baseKey: string) => { - const userPubkey = auth.currentUser?.value?.pubkey - return userPubkey ? `${baseKey}_${userPubkey}` : baseKey - } + const storageService = injectService(SERVICE_TOKENS.STORAGE_SERVICE) as any // Core market state const markets = ref([]) const stalls = ref([]) @@ -681,63 +677,42 @@ export const useMarketStore = defineStore('market', () => { // Persistence methods const saveOrdersToStorage = () => { - try { - const storageKey = getUserStorageKey('market_orders') - localStorage.setItem(storageKey, JSON.stringify(orders.value)) - - // Debug: Check what's being saved - const orderCount = Object.keys(orders.value).length - const paidOrders = Object.values(orders.value).filter(o => o.paymentStatus === 'paid' || o.status === 'paid') - - console.log('💾 Saved orders to localStorage:', { - storageKey, - totalOrders: orderCount, - paidOrders: paidOrders.length, - orderStatuses: Object.values(orders.value).map(o => ({ - id: o.id?.slice(-8), - status: o.status, - paymentStatus: o.paymentStatus, - hasPaymentRequest: !!o.paymentRequest - })) - }) - } catch (error) { - console.warn('Failed to save orders to localStorage:', error) - } + storageService.setUserData('market_orders', orders.value) + + // Debug: Check what's being saved + const orderCount = Object.keys(orders.value).length + const paidOrders = Object.values(orders.value).filter(o => o.paymentStatus === 'paid' || o.status === 'paid') + + console.log('💾 Saved orders to storage:', { + totalOrders: orderCount, + paidOrders: paidOrders.length, + orderStatuses: Object.values(orders.value).map(o => ({ + id: o.id?.slice(-8), + status: o.status, + paymentStatus: o.paymentStatus, + hasPaymentRequest: !!o.paymentRequest + })) + }) } const loadOrdersFromStorage = () => { - try { - const storageKey = getUserStorageKey('market_orders') - const stored = localStorage.getItem(storageKey) - if (stored) { - const parsedOrders = JSON.parse(stored) - orders.value = parsedOrders - - // Debug: Check payment status of loaded orders - const orderCount = Object.keys(parsedOrders).length - const paidOrders = Object.values(parsedOrders).filter((o: any) => o.paymentStatus === 'paid' || o.status === 'paid') - - console.log('📦 Loaded orders from localStorage:', { - storageKey, - totalOrders: orderCount, - paidOrders: paidOrders.length, - orderStatuses: Object.values(parsedOrders).map((o: any) => ({ - id: o.id?.slice(-8), - status: o.status, - paymentStatus: o.paymentStatus, - hasPaymentRequest: !!o.paymentRequest - })) - }) - } else { - console.log('No orders found in localStorage for key:', storageKey) - // Clear any existing orders when switching users - orders.value = {} - } - } catch (error) { - console.warn('Failed to load orders from localStorage:', error) - // Clear orders on error - orders.value = {} - } + const parsedOrders = storageService.getUserData('market_orders', {}) + orders.value = parsedOrders + + // Debug: Check payment status of loaded orders + const orderCount = Object.keys(parsedOrders).length + const paidOrders = Object.values(parsedOrders).filter((o: any) => o.paymentStatus === 'paid' || o.status === 'paid') + + console.log('📦 Loaded orders from storage:', { + totalOrders: orderCount, + paidOrders: paidOrders.length, + orderStatuses: Object.values(parsedOrders).map((o: any) => ({ + id: o.id?.slice(-8), + status: o.status, + paymentStatus: o.paymentStatus, + hasPaymentRequest: !!o.paymentRequest + })) + }) } // Clear orders when user changes