You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

21 KiB

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

// 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<PlanSummaryAndPreviousClaim>; 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:

// 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<PlanSummaryAndPreviousClaim>; 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:

// 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:

// 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<PlanSummaryAndPreviousClaim>; 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<PlanSummaryAndPreviousClaim>; 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:

// 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<void> {
    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<PlanSummaryAndPreviousClaim>; 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<Record<string, string>> {
    // 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<string> {
    // 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:

// 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<string, string>;
  
  // Enhanced request configuration
  requestConfig?: {
    method: 'GET' | 'POST' | 'PUT' | 'DELETE';
    url: string;
    headers?: Record<string, string>;
    body?: Record<string, unknown>;
    params?: Record<string, string>;
  };
}

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

// 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

// 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

// 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

// 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

// 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