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
- Keep existing TimeSafari PWA code unchanged
- Add plugin configuration alongside existing code
- Test plugin functionality in parallel
- Compare results between existing and plugin implementations
Phase 2: Gradual Migration
- Replace individual request methods one by one
- Use plugin's enhanced error handling and logging
- Maintain existing UI and user experience
- Add plugin-specific features (background fetching, etc.)
Phase 3: Full Integration
- Replace all TimeSafari request patterns with plugin
- Remove duplicate code
- Leverage plugin's advanced features
- 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:
- Implement parallel testing with existing TimeSafari PWA code
- Gradually migrate individual request methods
- Leverage plugin's advanced features for enhanced user experience