docs: add TimeSafari PWA request pattern adoption guide and example

- Add comprehensive guide for adopting existing TimeSafari PWA request patterns
- Show direct integration of loadNewStarredProjectChanges() and getStarredProjectsWithChanges()
- Provide Vue.js component integration examples
- Include migration strategy and testing approach
- Add practical example showing exact configuration needed
- Demonstrate parallel testing and performance comparison
- Show how to maintain existing TimeSafari interfaces while adding plugin features

This enables seamless adoption of the plugin into existing TimeSafari PWA codebase
while maintaining the same developer experience and user interface.
This commit is contained in:
Matthew Raymer
2025-10-08 07:19:46 +00:00
parent eaaa980167
commit ff166560df
2 changed files with 1085 additions and 0 deletions

View File

@@ -0,0 +1,682 @@
# 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<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:
```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<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:
```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<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:
```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<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:
```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<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
```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