# TimeSafari Daily Notification Plugin - Request Pattern Adoption Guide **Author**: Matthew Raymer **Version**: 1.0.0 **Created**: 2025-10-08 06:24:57 UTC ## Overview This guide shows how to directly adopt the existing TimeSafari PWA request patterns (like `loadNewStarredProjectChanges()` and `getStarredProjectsWithChanges()`) into the Daily Notification Plugin configuration. The plugin is designed to work seamlessly with TimeSafari's existing axios-based request architecture. ## Current TimeSafari PWA Pattern Analysis ### Original TimeSafari PWA Code ```typescript // TimeSafari PWA - HomeView.vue private async loadNewStarredProjectChanges() { if (this.activeDid && this.starredPlanHandleIds.length > 0) { try { const starredProjectChanges = await getStarredProjectsWithChanges( this.axios, this.apiServer, this.activeDid, this.starredPlanHandleIds, this.lastAckedStarredPlanChangesJwtId, ); this.numNewStarredProjectChanges = starredProjectChanges.data.length; this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; } catch (error) { logger.warn("[HomeView] Failed to load starred project changes:", error); this.numNewStarredProjectChanges = 0; this.newStarredProjectChangesHitLimit = false; } } } // TimeSafari PWA - API function export async function getStarredProjectsWithChanges( axios: Axios, apiServer: string, activeDid: string, starredPlanHandleIds: string[], afterId?: string, ): Promise<{ data: Array; hitLimit: boolean }> { const url = `${apiServer}/api/v2/report/plansLastUpdatedBetween`; const headers = await getHeaders(activeDid); const requestBody = { planIds: starredPlanHandleIds, afterId: afterId, }; const response = await axios.post(url, requestBody, { headers }); return response.data; } ``` ## Plugin Configuration Adoption ### 1. Direct Axios Integration Pattern The plugin can be configured to use the host's existing axios instance and request patterns: ```typescript // In TimeSafari PWA - Plugin Configuration import { DailyNotification } from '@timesafari/daily-notification-plugin'; import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; // Configure plugin with existing TimeSafari patterns await DailyNotification.configure({ // TimeSafari-specific configuration timesafariConfig: { activeDid: this.activeDid, // Use existing TimeSafari API endpoints endpoints: { offersToPerson: `${this.apiServer}/api/v2/offers/person`, offersToPlans: `${this.apiServer}/api/v2/offers/plans`, projectsLastUpdated: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween` }, // Configure starred projects fetching starredProjectsConfig: { enabled: true, starredPlanHandleIds: this.starredPlanHandleIds, lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, fetchInterval: '0 8 * * *', // Daily at 8 AM maxResults: 50, hitLimitHandling: 'warn' // 'warn' | 'error' | 'ignore' }, // Sync configuration matching TimeSafari patterns syncConfig: { enableParallel: true, maxConcurrent: 3, batchSize: 10, timeout: 30000, retryAttempts: 3 } }, // Network configuration using existing patterns networkConfig: { // Use existing axios instance httpClient: this.axios, baseURL: this.apiServer, timeout: 30000, retryAttempts: 3, retryDelay: 1000, // Headers matching TimeSafari pattern defaultHeaders: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }, // Content fetch configuration contentFetch: { enabled: true, schedule: '0 8 * * *', // Daily at 8 AM // Use existing TimeSafari request pattern requestConfig: { method: 'POST', url: '${apiServer}/api/v2/report/plansLastUpdatedBetween', headers: { 'Authorization': 'Bearer ${jwt}', 'X-User-DID': '${activeDid}' }, body: { planIds: '${starredPlanHandleIds}', afterId: '${lastAckedJwtId}' } }, // Callbacks matching TimeSafari error handling callbacks: { onSuccess: async (data: { data: Array; hitLimit: boolean }) => { // Handle successful fetch - same as TimeSafari PWA this.numNewStarredProjectChanges = data.data.length; this.newStarredProjectChangesHitLimit = data.hitLimit; // Update UI this.updateStarredProjectsUI(data); }, onError: async (error: Error) => { // Handle error - same as TimeSafari PWA logger.warn("[DailyNotification] Failed to load starred project changes:", error); this.numNewStarredProjectChanges = 0; this.newStarredProjectChangesHitLimit = false; }, onComplete: async (result: ContentFetchResult) => { // Handle completion logger.info("[DailyNotification] Starred projects fetch completed", { success: result.success, dataCount: result.data?.data?.length || 0, hitLimit: result.data?.hitLimit || false }); } } } }); ``` ### 2. Enhanced TimeSafari Integration Service The plugin provides an enhanced integration service that mirrors TimeSafari's patterns: ```typescript // In TimeSafari PWA - Enhanced Integration import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; // Initialize with existing TimeSafari configuration const integrationService = TimeSafariIntegrationService.getInstance(); await integrationService.initialize({ activeDid: this.activeDid, storageAdapter: this.timeSafariStorageAdapter, endorserApiBaseUrl: this.apiServer, // Use existing TimeSafari request patterns requestConfig: { httpClient: this.axios, baseURL: this.apiServer, timeout: 30000, retryAttempts: 3 }, // Configure starred projects fetching starredProjectsConfig: { enabled: true, starredPlanHandleIds: this.starredPlanHandleIds, lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, fetchInterval: '0 8 * * *', maxResults: 50 } }); // Use the service to fetch starred projects (same pattern as TimeSafari PWA) const fetchStarredProjects = async () => { try { const starredProjectChanges = await integrationService.getStarredProjectsWithChanges( this.activeDid, this.starredPlanHandleIds, this.lastAckedStarredPlanChangesJwtId ); // Same handling as TimeSafari PWA this.numNewStarredProjectChanges = starredProjectChanges.data.length; this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; } catch (error) { // Same error handling as TimeSafari PWA logger.warn("[DailyNotification] Failed to load starred project changes:", error); this.numNewStarredProjectChanges = 0; this.newStarredProjectChangesHitLimit = false; } }; ``` ### 3. Vue.js Component Integration Direct integration into existing TimeSafari Vue components: ```typescript // In TimeSafari PWA - HomeView.vue (enhanced) import { defineComponent } from 'vue'; import { DailyNotification } from '@timesafari/daily-notification-plugin'; import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; export default defineComponent({ name: 'HomeView', data() { return { // Existing TimeSafari data activeDid: '', starredPlanHandleIds: [] as string[], lastAckedStarredPlanChangesJwtId: '', numNewStarredProjectChanges: 0, newStarredProjectChangesHitLimit: false, // Plugin integration dailyNotificationService: null as DailyNotificationService | null, integrationService: null as TimeSafariIntegrationService | null }; }, async mounted() { // Initialize plugin with existing TimeSafari configuration await this.initializeDailyNotifications(); }, methods: { async initializeDailyNotifications() { try { // Configure plugin with existing TimeSafari patterns await DailyNotification.configure({ timesafariConfig: { activeDid: this.activeDid, endpoints: { projectsLastUpdated: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween` }, starredProjectsConfig: { enabled: true, starredPlanHandleIds: this.starredPlanHandleIds, lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, fetchInterval: '0 8 * * *' } }, networkConfig: { httpClient: this.axios, baseURL: this.apiServer, timeout: 30000 }, contentFetch: { enabled: true, schedule: '0 8 * * *', callbacks: { onSuccess: this.handleStarredProjectsSuccess, onError: this.handleStarredProjectsError } } }); // Initialize integration service this.integrationService = TimeSafariIntegrationService.getInstance(); await this.integrationService.initialize({ activeDid: this.activeDid, storageAdapter: this.timeSafariStorageAdapter, endorserApiBaseUrl: this.apiServer }); // Replace existing method with plugin-enhanced version this.loadNewStarredProjectChanges = this.loadNewStarredProjectChangesEnhanced; } catch (error) { logger.error("[HomeView] Failed to initialize daily notifications:", error); } }, // Enhanced version of existing method async loadNewStarredProjectChangesEnhanced() { if (this.activeDid && this.starredPlanHandleIds.length > 0) { try { // Use plugin's enhanced fetching with same interface const starredProjectChanges = await this.integrationService!.getStarredProjectsWithChanges( this.activeDid, this.starredPlanHandleIds, this.lastAckedStarredPlanChangesJwtId ); // Same handling as original TimeSafari PWA this.numNewStarredProjectChanges = starredProjectChanges.data.length; this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; } catch (error) { // Same error handling as original TimeSafari PWA logger.warn("[HomeView] Failed to load starred project changes:", error); this.numNewStarredProjectChanges = 0; this.newStarredProjectChangesHitLimit = false; } } else { this.numNewStarredProjectChanges = 0; this.newStarredProjectChangesHitLimit = false; } }, // Callback handlers matching TimeSafari patterns async handleStarredProjectsSuccess(data: { data: Array; hitLimit: boolean }) { // Same handling as TimeSafari PWA this.numNewStarredProjectChanges = data.data.length; this.newStarredProjectChangesHitLimit = data.hitLimit; // Update UI this.updateStarredProjectsUI(data); }, async handleStarredProjectsError(error: Error) { // Same error handling as TimeSafari PWA logger.warn("[HomeView] Failed to load starred project changes:", error); this.numNewStarredProjectChanges = 0; this.newStarredProjectChangesHitLimit = false; }, // Existing TimeSafari methods (unchanged) updateStarredProjectsUI(data: { data: Array; hitLimit: boolean }) { // Existing TimeSafari UI update logic this.$emit('starred-projects-updated', data); } } }); ``` ## Plugin Implementation Details ### 1. Enhanced TimeSafari Integration Service The plugin provides an enhanced version of TimeSafari's request patterns: ```typescript // Plugin implementation - Enhanced TimeSafari Integration Service export class TimeSafariIntegrationService { private static instance: TimeSafariIntegrationService; private httpClient: AxiosInstance | null = null; private baseURL: string = ''; private activeDid: string = ''; static getInstance(): TimeSafariIntegrationService { if (!TimeSafariIntegrationService.instance) { TimeSafariIntegrationService.instance = new TimeSafariIntegrationService(); } return TimeSafariIntegrationService.instance; } async initialize(config: { activeDid: string; httpClient: AxiosInstance; baseURL: string; storageAdapter: TimeSafariStorageAdapter; endorserApiBaseUrl: string; }): Promise { this.activeDid = config.activeDid; this.httpClient = config.httpClient; this.baseURL = config.baseURL; // Initialize with existing TimeSafari patterns await this.initializeStorage(config.storageAdapter); await this.initializeAuthentication(config.endorserApiBaseUrl); } // Enhanced version of TimeSafari's getStarredProjectsWithChanges async getStarredProjectsWithChanges( activeDid: string, starredPlanHandleIds: string[], afterId?: string ): Promise<{ data: Array; hitLimit: boolean }> { if (!starredPlanHandleIds || starredPlanHandleIds.length === 0) { return { data: [], hitLimit: false }; } if (!afterId) { return { data: [], hitLimit: false }; } try { // Use existing TimeSafari request pattern const url = `${this.baseURL}/api/v2/report/plansLastUpdatedBetween`; const headers = await this.getHeaders(activeDid); const requestBody = { planIds: starredPlanHandleIds, afterId: afterId, }; // Use host's axios instance const response = await this.httpClient!.post(url, requestBody, { headers }); // Log with structured logging observability.logEvent('INFO', EVENT_CODES.FETCH_SUCCESS, 'Starred projects fetched successfully', { activeDid, planCount: starredPlanHandleIds.length, resultCount: response.data.data.length, hitLimit: response.data.hitLimit }); return response.data; } catch (error) { // Enhanced error handling with structured logging observability.logEvent('ERROR', EVENT_CODES.FETCH_FAILURE, 'Failed to fetch starred projects', { activeDid, planCount: starredPlanHandleIds.length, error: (error as Error).message }); throw error; } } // Enhanced version of TimeSafari's getHeaders private async getHeaders(activeDid: string): Promise> { // Use existing TimeSafari authentication pattern const jwt = await this.getJWTToken(activeDid); return { 'Authorization': `Bearer ${jwt}`, 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-User-DID': activeDid }; } // Enhanced JWT token management private async getJWTToken(activeDid: string): Promise { // Use existing TimeSafari JWT pattern const cachedToken = await this.getCachedToken(activeDid); if (cachedToken && !this.isTokenExpired(cachedToken)) { return cachedToken; } // Generate new token using existing TimeSafari pattern const newToken = await this.generateJWTToken(activeDid); await this.cacheToken(activeDid, newToken); return newToken; } } ``` ### 2. Plugin Configuration Extensions The plugin extends the configuration to support TimeSafari's existing patterns: ```typescript // Plugin configuration extensions export interface TimeSafariConfig { activeDid: string; // Existing TimeSafari endpoints endpoints?: { offersToPerson?: string; offersToPlans?: string; projectsLastUpdated?: string; }; // Enhanced starred projects configuration starredProjectsConfig?: { enabled: boolean; starredPlanHandleIds: string[]; lastAckedJwtId: string; fetchInterval: string; // Cron expression maxResults?: number; hitLimitHandling?: 'warn' | 'error' | 'ignore'; }; // Existing TimeSafari sync configuration syncConfig?: { enableParallel?: boolean; maxConcurrent?: number; batchSize?: number; timeout?: number; retryAttempts?: number; }; // Enhanced error policy errorPolicy?: { maxRetries?: number; backoffMultiplier?: number; activeDidChangeRetries?: number; starredProjectsRetries?: number; // New }; } export interface NetworkConfig { // Use existing TimeSafari axios instance httpClient?: AxiosInstance; baseURL?: string; timeout?: number; retryAttempts?: number; retryDelay?: number; // Existing TimeSafari headers defaultHeaders?: Record; // Enhanced request configuration requestConfig?: { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; url: string; headers?: Record; body?: Record; params?: Record; }; } ``` ## Migration Strategy ### Phase 1: Parallel Implementation 1. **Keep existing TimeSafari PWA code unchanged** 2. **Add plugin configuration alongside existing code** 3. **Test plugin functionality in parallel** 4. **Compare results between existing and plugin implementations** ### Phase 2: Gradual Migration 1. **Replace individual request methods one by one** 2. **Use plugin's enhanced error handling and logging** 3. **Maintain existing UI and user experience** 4. **Add plugin-specific features (background fetching, etc.)** ### Phase 3: Full Integration 1. **Replace all TimeSafari request patterns with plugin** 2. **Remove duplicate code** 3. **Leverage plugin's advanced features** 4. **Optimize performance with plugin's caching and batching** ## Benefits of Plugin Adoption ### 1. Enhanced Error Handling ```typescript // Existing TimeSafari PWA catch (error) { logger.warn("[HomeView] Failed to load starred project changes:", error); this.numNewStarredProjectChanges = 0; } // Plugin-enhanced version catch (error) { // Structured logging with event IDs observability.logEvent('WARN', EVENT_CODES.FETCH_FAILURE, 'Failed to load starred project changes', { eventId: this.generateEventId(), activeDid: this.activeDid, planCount: this.starredPlanHandleIds.length, error: error.message, retryCount: this.retryCount }); // Enhanced fallback handling await this.handleStarredProjectsFallback(error); } ``` ### 2. Background Fetching ```typescript // Plugin provides background fetching await DailyNotification.configure({ contentFetch: { enabled: true, schedule: '0 8 * * *', // Daily at 8 AM backgroundFetch: true, // Fetch in background cachePolicy: { maxAge: 3600, // 1 hour cache staleWhileRevalidate: 1800 // 30 minutes stale } } }); ``` ### 3. Enhanced Observability ```typescript // Plugin provides comprehensive metrics const metrics = await DailyNotification.getMetrics(); console.log('Starred Projects Metrics:', { fetchSuccessRate: metrics.starredProjects.fetchSuccessRate, averageResponseTime: metrics.starredProjects.averageResponseTime, cacheHitRate: metrics.starredProjects.cacheHitRate, errorRate: metrics.starredProjects.errorRate }); ``` ## Testing Strategy ### 1. Parallel Testing ```typescript // Test both implementations in parallel const testStarredProjectsFetch = async () => { // Existing TimeSafari PWA implementation const existingResult = await getStarredProjectsWithChanges( this.axios, this.apiServer, this.activeDid, this.starredPlanHandleIds, this.lastAckedStarredPlanChangesJwtId ); // Plugin implementation const pluginResult = await this.integrationService.getStarredProjectsWithChanges( this.activeDid, this.starredPlanHandleIds, this.lastAckedStarredPlanChangesJwtId ); // Compare results assert.deepEqual(existingResult, pluginResult); }; ``` ### 2. Performance Testing ```typescript // Compare performance const performanceTest = async () => { const start = Date.now(); // Existing implementation await getStarredProjectsWithChanges(...); const existingTime = Date.now() - start; const pluginStart = Date.now(); // Plugin implementation await this.integrationService.getStarredProjectsWithChanges(...); const pluginTime = Date.now() - pluginStart; console.log('Performance Comparison:', { existing: existingTime, plugin: pluginTime, improvement: ((existingTime - pluginTime) / existingTime * 100).toFixed(2) + '%' }); }; ``` ## Conclusion The Daily Notification Plugin is designed to seamlessly adopt TimeSafari's existing request patterns while providing enhanced functionality: - **Same Interface**: Plugin methods match existing TimeSafari patterns - **Enhanced Features**: Background fetching, structured logging, metrics - **Gradual Migration**: Can be adopted incrementally - **Backward Compatibility**: Existing code continues to work - **Performance Improvements**: Caching, batching, and optimization The plugin transforms TimeSafari's existing `loadNewStarredProjectChanges()` pattern into a more robust, observable, and efficient system while maintaining the same developer experience and user interface. --- **Next Steps**: 1. Implement parallel testing with existing TimeSafari PWA code 2. Gradually migrate individual request methods 3. Leverage plugin's advanced features for enhanced user experience