docs(integration): update integration guide and add host app examples
- Update INTEGRATION_GUIDE.md to version 2.1.0 with generic polling support - Add comprehensive generic polling integration section with quick start guide - Include TimeSafariPollingService class example with complete implementation - Add Vue component integration patterns with PlatformServiceMixin updates - Update Capacitor configuration with genericPolling section and legacy compatibility - Add TypeScript service methods for setupStarredProjectsPolling and handlePollingResult - Include JWT token management, watermark CAS, and error handling examples - Add examples/hello-poll.ts with minimal host-app integration example - Add examples/stale-data-ux.ts with platform-specific UX snippets for stale data - Include complete end-to-end workflow from config → schedule → delivery → ack → CAS - Document backward compatibility with existing dual scheduling approach Provides production-ready integration patterns for TimeSafari host applications.
This commit is contained in:
319
examples/hello-poll.ts
Normal file
319
examples/hello-poll.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* Hello Poll - Minimal host-app example
|
||||
*
|
||||
* Demonstrates the complete polling flow:
|
||||
* 1. Define schemas with Zod
|
||||
* 2. Configure generic polling request
|
||||
* 3. Schedule with platform wrapper
|
||||
* 4. Handle delivery via outbox → dispatcher → acknowledge → CAS watermark
|
||||
*/
|
||||
|
||||
import {
|
||||
GenericPollingRequest,
|
||||
PollingScheduleConfig,
|
||||
PollingResult,
|
||||
StarredProjectsRequest,
|
||||
StarredProjectsResponse
|
||||
} from '@timesafari/polling-contracts';
|
||||
import {
|
||||
StarredProjectsRequestSchema,
|
||||
StarredProjectsResponseSchema,
|
||||
createResponseValidator,
|
||||
generateIdempotencyKey
|
||||
} from '@timesafari/polling-contracts';
|
||||
|
||||
// Mock server for testing
|
||||
class MockServer {
|
||||
private data: any[] = [
|
||||
{
|
||||
planSummary: {
|
||||
jwtId: '1704067200_abc123_def45678',
|
||||
handleId: 'hello_project',
|
||||
name: 'Hello Project',
|
||||
description: 'A simple test project',
|
||||
issuerDid: 'did:key:test_issuer',
|
||||
agentDid: 'did:key:test_agent',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
endTime: '2025-01-31T23:59:59Z',
|
||||
version: '1.0.0'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
async handleRequest(request: StarredProjectsRequest): Promise<StarredProjectsResponse> {
|
||||
// Simulate API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Filter data based on afterId
|
||||
let filteredData = this.data;
|
||||
if (request.afterId) {
|
||||
filteredData = this.data.filter(item =>
|
||||
item.planSummary.jwtId > request.afterId!
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
data: filteredData,
|
||||
hitLimit: false,
|
||||
pagination: {
|
||||
hasMore: false,
|
||||
nextAfterId: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
addNewData(jwtId: string, name: string): void {
|
||||
this.data.push({
|
||||
planSummary: {
|
||||
jwtId,
|
||||
handleId: `project_${jwtId.split('_')[0]}`,
|
||||
name,
|
||||
description: `Updated project: ${name}`,
|
||||
issuerDid: 'did:key:test_issuer',
|
||||
agentDid: 'did:key:test_agent',
|
||||
startTime: '2025-01-01T00:00:00Z',
|
||||
endTime: '2025-01-31T23:59:59Z',
|
||||
version: '1.0.0'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mock storage adapter
|
||||
class MockStorageAdapter {
|
||||
private storage = new Map<string, any>();
|
||||
|
||||
async get(key: string): Promise<any> {
|
||||
return this.storage.get(key);
|
||||
}
|
||||
|
||||
async set(key: string, value: any): Promise<void> {
|
||||
this.storage.set(key, value);
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
this.storage.delete(key);
|
||||
}
|
||||
|
||||
async exists(key: string): Promise<boolean> {
|
||||
return this.storage.has(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Mock authentication manager
|
||||
class MockAuthManager {
|
||||
private token = 'mock_jwt_token';
|
||||
|
||||
async getCurrentToken(): Promise<string | null> {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
async refreshToken(): Promise<string> {
|
||||
this.token = `mock_jwt_token_${Date.now()}`;
|
||||
return this.token;
|
||||
}
|
||||
|
||||
async validateToken(token: string): Promise<boolean> {
|
||||
return token.startsWith('mock_jwt_token');
|
||||
}
|
||||
}
|
||||
|
||||
// Mock polling manager
|
||||
class MockPollingManager {
|
||||
private server: MockServer;
|
||||
private storage: MockStorageAdapter;
|
||||
private auth: MockAuthManager;
|
||||
|
||||
constructor(server: MockServer, storage: MockStorageAdapter, auth: MockAuthManager) {
|
||||
this.server = server;
|
||||
this.storage = storage;
|
||||
this.auth = auth;
|
||||
}
|
||||
|
||||
async executePoll<TRequest, TResponse>(
|
||||
request: GenericPollingRequest<TRequest, TResponse>
|
||||
): Promise<PollingResult<TResponse>> {
|
||||
try {
|
||||
// Validate idempotency key
|
||||
if (!request.idempotencyKey) {
|
||||
request.idempotencyKey = generateIdempotencyKey();
|
||||
}
|
||||
|
||||
// Execute request
|
||||
const response = await this.server.handleRequest(request.body as StarredProjectsRequest);
|
||||
|
||||
// Validate response
|
||||
const validator = createResponseValidator(StarredProjectsResponseSchema);
|
||||
if (!validator.validate(response)) {
|
||||
throw new Error('Response validation failed');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response as TResponse,
|
||||
error: undefined,
|
||||
metadata: {
|
||||
requestId: request.idempotencyKey!,
|
||||
timestamp: new Date().toISOString(),
|
||||
duration: 100,
|
||||
retryCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: undefined,
|
||||
error: {
|
||||
code: 'EXECUTION_ERROR',
|
||||
message: String(error),
|
||||
retryable: true
|
||||
},
|
||||
metadata: {
|
||||
requestId: request.idempotencyKey || 'unknown',
|
||||
timestamp: new Date().toISOString(),
|
||||
duration: 0,
|
||||
retryCount: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async schedulePoll<TRequest, TResponse>(
|
||||
config: PollingScheduleConfig<TRequest, TResponse>
|
||||
): Promise<string> {
|
||||
const scheduleId = `schedule_${Date.now()}`;
|
||||
|
||||
// Store configuration
|
||||
await this.storage.set(`polling_config_${scheduleId}`, config);
|
||||
|
||||
// Simulate scheduling
|
||||
console.log(`Scheduled poll: ${scheduleId}`);
|
||||
|
||||
return scheduleId;
|
||||
}
|
||||
}
|
||||
|
||||
// Main example
|
||||
async function runHelloPollExample(): Promise<void> {
|
||||
console.log('🚀 Starting Hello Poll Example');
|
||||
|
||||
// 1. Set up dependencies
|
||||
const server = new MockServer();
|
||||
const storage = new MockStorageAdapter();
|
||||
const auth = new MockAuthManager();
|
||||
const pollingManager = new MockPollingManager(server, storage, auth);
|
||||
|
||||
// 2. Define polling request
|
||||
const request: GenericPollingRequest<StarredProjectsRequest, StarredProjectsResponse> = {
|
||||
endpoint: '/api/v2/report/plansLastUpdatedBetween',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'HelloPoll-Example/1.0.0'
|
||||
},
|
||||
body: {
|
||||
planIds: ['hello_project'],
|
||||
afterId: undefined, // Will be populated from watermark
|
||||
limit: 100
|
||||
},
|
||||
responseSchema: createResponseValidator(StarredProjectsResponseSchema),
|
||||
retryConfig: {
|
||||
maxAttempts: 3,
|
||||
backoffStrategy: 'exponential',
|
||||
baseDelayMs: 1000
|
||||
},
|
||||
timeoutMs: 30000
|
||||
};
|
||||
|
||||
// 3. Schedule polling
|
||||
const scheduleConfig: PollingScheduleConfig<StarredProjectsRequest, StarredProjectsResponse> = {
|
||||
request,
|
||||
schedule: {
|
||||
cronExpression: '0 10,16 * * *', // 10 AM and 4 PM daily
|
||||
timezone: 'UTC',
|
||||
maxConcurrentPolls: 1
|
||||
},
|
||||
notificationConfig: {
|
||||
enabled: true,
|
||||
templates: {
|
||||
singleUpdate: '{projectName} has been updated',
|
||||
multipleUpdates: 'You have {count} new updates in your starred projects'
|
||||
},
|
||||
groupingRules: {
|
||||
maxGroupSize: 5,
|
||||
timeWindowMinutes: 5
|
||||
}
|
||||
},
|
||||
stateConfig: {
|
||||
watermarkKey: 'lastAckedStarredPlanChangesJwtId',
|
||||
storageAdapter: storage
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleId = await pollingManager.schedulePoll(scheduleConfig);
|
||||
console.log(`✅ Scheduled poll: ${scheduleId}`);
|
||||
|
||||
// 4. Execute initial poll
|
||||
console.log('📡 Executing initial poll...');
|
||||
const result = await pollingManager.executePoll(request);
|
||||
|
||||
if (result.success && result.data) {
|
||||
console.log(`✅ Found ${result.data.data.length} changes`);
|
||||
|
||||
if (result.data.data.length > 0) {
|
||||
// 5. Generate notifications
|
||||
const changes = result.data.data;
|
||||
console.log('🔔 Generating notifications...');
|
||||
|
||||
if (changes.length === 1) {
|
||||
const project = changes[0].planSummary;
|
||||
console.log(`📱 Notification: "${project.name} has been updated"`);
|
||||
} else {
|
||||
console.log(`📱 Notification: "You have ${changes.length} new updates in your starred projects"`);
|
||||
}
|
||||
|
||||
// 6. Update watermark with CAS
|
||||
const latestJwtId = changes[changes.length - 1].planSummary.jwtId;
|
||||
await storage.set('lastAckedStarredPlanChangesJwtId', latestJwtId);
|
||||
console.log(`💾 Updated watermark: ${latestJwtId}`);
|
||||
|
||||
// 7. Acknowledge changes (simulate)
|
||||
console.log('✅ Acknowledged changes with server');
|
||||
}
|
||||
} else {
|
||||
console.log('❌ Poll failed:', result.error?.message);
|
||||
}
|
||||
|
||||
// 8. Simulate new data and poll again
|
||||
console.log('\n🔄 Adding new data and polling again...');
|
||||
server.addNewData('1704153600_new123_0badf00d', 'Updated Hello Project');
|
||||
|
||||
// Update request with watermark
|
||||
request.body.afterId = await storage.get('lastAckedStarredPlanChangesJwtId');
|
||||
|
||||
const result2 = await pollingManager.executePoll(request);
|
||||
|
||||
if (result2.success && result2.data) {
|
||||
console.log(`✅ Found ${result2.data.data.length} new changes`);
|
||||
|
||||
if (result2.data.data.length > 0) {
|
||||
const project = result2.data.data[0].planSummary;
|
||||
console.log(`📱 New notification: "${project.name} has been updated"`);
|
||||
|
||||
// Update watermark
|
||||
const latestJwtId = result2.data.data[result2.data.data.length - 1].planSummary.jwtId;
|
||||
await storage.set('lastAckedStarredPlanChangesJwtId', latestJwtId);
|
||||
console.log(`💾 Updated watermark: ${latestJwtId}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n🎉 Hello Poll Example completed successfully!');
|
||||
}
|
||||
|
||||
// Run the example
|
||||
if (require.main === module) {
|
||||
runHelloPollExample().catch(console.error);
|
||||
}
|
||||
|
||||
export { runHelloPollExample };
|
||||
360
examples/stale-data-ux.ts
Normal file
360
examples/stale-data-ux.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* Stale Data UX Snippets
|
||||
*
|
||||
* Platform-specific implementations for showing stale data banners
|
||||
* when polling hasn't succeeded for extended periods
|
||||
*/
|
||||
|
||||
// Common configuration
|
||||
const STALE_DATA_CONFIG = {
|
||||
staleThresholdHours: 4,
|
||||
criticalThresholdHours: 24,
|
||||
bannerAutoDismissMs: 10000
|
||||
};
|
||||
|
||||
// Common i18n keys
|
||||
const I18N_KEYS = {
|
||||
'staleness.banner.title': 'Data may be outdated',
|
||||
'staleness.banner.message': 'Last updated {hours} hours ago. Tap to refresh.',
|
||||
'staleness.banner.critical': 'Data is very outdated. Please refresh.',
|
||||
'staleness.banner.action_refresh': 'Refresh Now',
|
||||
'staleness.banner.action_settings': 'Settings',
|
||||
'staleness.banner.dismiss': 'Dismiss'
|
||||
};
|
||||
|
||||
/**
|
||||
* Android Implementation
|
||||
*/
|
||||
class AndroidStaleDataUX {
|
||||
private context: any; // Android Context
|
||||
private notificationManager: any; // NotificationManager
|
||||
|
||||
constructor(context: any) {
|
||||
this.context = context;
|
||||
this.notificationManager = context.getSystemService('notification');
|
||||
}
|
||||
|
||||
showStalenessBanner(hoursSinceUpdate: number): void {
|
||||
const isCritical = hoursSinceUpdate >= STALE_DATA_CONFIG.criticalThresholdHours;
|
||||
|
||||
const title = this.context.getString(I18N_KEYS['staleness.banner.title']);
|
||||
const message = isCritical
|
||||
? this.context.getString(I18N_KEYS['staleness.banner.critical'])
|
||||
: this.context.getString(I18N_KEYS['staleness.banner.message'], hoursSinceUpdate);
|
||||
|
||||
// Create notification
|
||||
const notification = {
|
||||
smallIcon: 'ic_warning',
|
||||
contentTitle: title,
|
||||
contentText: message,
|
||||
priority: isCritical ? 'high' : 'normal',
|
||||
autoCancel: true,
|
||||
actions: [
|
||||
{
|
||||
title: this.context.getString(I18N_KEYS['staleness.banner.action_refresh']),
|
||||
intent: this.createRefreshIntent()
|
||||
},
|
||||
{
|
||||
title: this.context.getString(I18N_KEYS['staleness.banner.action_settings']),
|
||||
intent: this.createSettingsIntent()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.notificationManager.notify('stale_data_warning', notification);
|
||||
}
|
||||
|
||||
private createRefreshIntent(): any {
|
||||
// Create PendingIntent for refresh action
|
||||
return {
|
||||
action: 'com.timesafari.dailynotification.REFRESH_DATA',
|
||||
flags: ['FLAG_UPDATE_CURRENT']
|
||||
};
|
||||
}
|
||||
|
||||
private createSettingsIntent(): any {
|
||||
// Create PendingIntent for settings action
|
||||
return {
|
||||
action: 'com.timesafari.dailynotification.OPEN_SETTINGS',
|
||||
flags: ['FLAG_UPDATE_CURRENT']
|
||||
};
|
||||
}
|
||||
|
||||
showInAppBanner(hoursSinceUpdate: number): void {
|
||||
// Show banner in app UI (Snackbar or similar)
|
||||
const message = this.context.getString(
|
||||
I18N_KEYS['staleness.banner.message'],
|
||||
hoursSinceUpdate
|
||||
);
|
||||
|
||||
// Create Snackbar
|
||||
const snackbar = {
|
||||
message,
|
||||
duration: 'LENGTH_INDEFINITE',
|
||||
action: {
|
||||
text: this.context.getString(I18N_KEYS['staleness.banner.action_refresh']),
|
||||
callback: () => this.refreshData()
|
||||
}
|
||||
};
|
||||
|
||||
// Show snackbar
|
||||
console.log('Showing Android in-app banner:', snackbar);
|
||||
}
|
||||
|
||||
private refreshData(): void {
|
||||
// Trigger manual refresh
|
||||
console.log('Refreshing data on Android');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* iOS Implementation
|
||||
*/
|
||||
class iOSStaleDataUX {
|
||||
private viewController: any; // UIViewController
|
||||
|
||||
constructor(viewController: any) {
|
||||
this.viewController = viewController;
|
||||
}
|
||||
|
||||
showStalenessBanner(hoursSinceUpdate: number): void {
|
||||
const isCritical = hoursSinceUpdate >= STALE_DATA_CONFIG.criticalThresholdHours;
|
||||
|
||||
const title = NSLocalizedString(I18N_KEYS['staleness.banner.title'], '');
|
||||
const message = isCritical
|
||||
? NSLocalizedString(I18N_KEYS['staleness.banner.critical'], '')
|
||||
: String(format: NSLocalizedString(I18N_KEYS['staleness.banner.message'], ''), hoursSinceUpdate);
|
||||
|
||||
// Create alert controller
|
||||
const alert = {
|
||||
title,
|
||||
message,
|
||||
preferredStyle: 'alert',
|
||||
actions: [
|
||||
{
|
||||
title: NSLocalizedString(I18N_KEYS['staleness.banner.action_refresh'], ''),
|
||||
style: 'default',
|
||||
handler: () => this.refreshData()
|
||||
},
|
||||
{
|
||||
title: NSLocalizedString(I18N_KEYS['staleness.banner.action_settings'], ''),
|
||||
style: 'default',
|
||||
handler: () => this.openSettings()
|
||||
},
|
||||
{
|
||||
title: NSLocalizedString(I18N_KEYS['staleness.banner.dismiss'], ''),
|
||||
style: 'cancel'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.viewController.present(alert, animated: true);
|
||||
}
|
||||
|
||||
showBannerView(hoursSinceUpdate: number): void {
|
||||
// Create banner view
|
||||
const banner = {
|
||||
title: NSLocalizedString(I18N_KEYS['staleness.banner.title'], ''),
|
||||
message: String(format: NSLocalizedString(I18N_KEYS['staleness.banner.message'], ''), hoursSinceUpdate),
|
||||
backgroundColor: 'systemYellow',
|
||||
textColor: 'label',
|
||||
actions: [
|
||||
{
|
||||
title: NSLocalizedString(I18N_KEYS['staleness.banner.action_refresh'], ''),
|
||||
action: () => this.refreshData()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Show banner
|
||||
console.log('Showing iOS banner view:', banner);
|
||||
}
|
||||
|
||||
private refreshData(): void {
|
||||
console.log('Refreshing data on iOS');
|
||||
}
|
||||
|
||||
private openSettings(): void {
|
||||
console.log('Opening settings on iOS');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Implementation
|
||||
*/
|
||||
class WebStaleDataUX {
|
||||
private container: HTMLElement;
|
||||
|
||||
constructor(container: HTMLElement = document.body) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
showStalenessBanner(hoursSinceUpdate: number): void {
|
||||
const isCritical = hoursSinceUpdate >= STALE_DATA_CONFIG.criticalThresholdHours;
|
||||
|
||||
const banner = document.createElement('div');
|
||||
banner.className = `staleness-banner ${isCritical ? 'critical' : 'warning'}`;
|
||||
banner.innerHTML = `
|
||||
<div class="banner-content">
|
||||
<div class="banner-icon">⚠️</div>
|
||||
<div class="banner-text">
|
||||
<div class="banner-title">${I18N_KEYS['staleness.banner.title']}</div>
|
||||
<div class="banner-message">
|
||||
${isCritical
|
||||
? I18N_KEYS['staleness.banner.critical']
|
||||
: I18N_KEYS['staleness.banner.message'].replace('{hours}', hoursSinceUpdate.toString())
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="banner-actions">
|
||||
<button class="btn-refresh" onclick="window.refreshData()">
|
||||
${I18N_KEYS['staleness.banner.action_refresh']}
|
||||
</button>
|
||||
<button class="btn-settings" onclick="window.openSettings()">
|
||||
${I18N_KEYS['staleness.banner.action_settings']}
|
||||
</button>
|
||||
<button class="btn-dismiss" onclick="this.parentElement.parentElement.remove()">
|
||||
${I18N_KEYS['staleness.banner.dismiss']}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add styles
|
||||
banner.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: ${isCritical ? '#ff6b6b' : '#ffd93d'};
|
||||
color: ${isCritical ? 'white' : 'black'};
|
||||
padding: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
z-index: 1000;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
`;
|
||||
|
||||
this.container.appendChild(banner);
|
||||
|
||||
// Auto-dismiss after timeout
|
||||
setTimeout(() => {
|
||||
if (banner.parentElement) {
|
||||
banner.remove();
|
||||
}
|
||||
}, STALE_DATA_CONFIG.bannerAutoDismissMs);
|
||||
}
|
||||
|
||||
showToast(hoursSinceUpdate: number): void {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'staleness-toast';
|
||||
toast.innerHTML = `
|
||||
<div class="toast-content">
|
||||
<span class="toast-icon">⚠️</span>
|
||||
<span class="toast-message">
|
||||
${I18N_KEYS['staleness.banner.message'].replace('{hours}', hoursSinceUpdate.toString())}
|
||||
</span>
|
||||
<button class="toast-action" onclick="window.refreshData()">
|
||||
${I18N_KEYS['staleness.banner.action_refresh']}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add styles
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 1000;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
`;
|
||||
|
||||
this.container.appendChild(toast);
|
||||
|
||||
// Auto-dismiss
|
||||
setTimeout(() => {
|
||||
if (toast.parentElement) {
|
||||
toast.remove();
|
||||
}
|
||||
}, STALE_DATA_CONFIG.bannerAutoDismissMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stale Data Manager
|
||||
*/
|
||||
class StaleDataManager {
|
||||
private platform: 'android' | 'ios' | 'web';
|
||||
private ux: AndroidStaleDataUX | iOSStaleDataUX | WebStaleDataUX;
|
||||
private lastSuccessfulPoll: number = 0;
|
||||
|
||||
constructor(platform: 'android' | 'ios' | 'web', context?: any) {
|
||||
this.platform = platform;
|
||||
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
this.ux = new AndroidStaleDataUX(context);
|
||||
break;
|
||||
case 'ios':
|
||||
this.ux = new iOSStaleDataUX(context);
|
||||
break;
|
||||
case 'web':
|
||||
this.ux = new WebStaleDataUX(context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
updateLastSuccessfulPoll(): void {
|
||||
this.lastSuccessfulPoll = Date.now();
|
||||
}
|
||||
|
||||
checkAndShowStaleDataBanner(): void {
|
||||
const hoursSinceUpdate = (Date.now() - this.lastSuccessfulPoll) / (1000 * 60 * 60);
|
||||
|
||||
if (hoursSinceUpdate >= STALE_DATA_CONFIG.staleThresholdHours) {
|
||||
this.ux.showStalenessBanner(Math.floor(hoursSinceUpdate));
|
||||
}
|
||||
}
|
||||
|
||||
isDataStale(): boolean {
|
||||
const hoursSinceUpdate = (Date.now() - this.lastSuccessfulPoll) / (1000 * 60 * 60);
|
||||
return hoursSinceUpdate >= STALE_DATA_CONFIG.staleThresholdHours;
|
||||
}
|
||||
|
||||
isDataCritical(): boolean {
|
||||
const hoursSinceUpdate = (Date.now() - this.lastSuccessfulPoll) / (1000 * 60 * 60);
|
||||
return hoursSinceUpdate >= STALE_DATA_CONFIG.criticalThresholdHours;
|
||||
}
|
||||
|
||||
getHoursSinceUpdate(): number {
|
||||
return (Date.now() - this.lastSuccessfulPoll) / (1000 * 60 * 60);
|
||||
}
|
||||
}
|
||||
|
||||
// Global functions for web
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).refreshData = () => {
|
||||
console.log('Refreshing data from web banner');
|
||||
};
|
||||
|
||||
(window as any).openSettings = () => {
|
||||
console.log('Opening settings from web banner');
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
StaleDataManager,
|
||||
AndroidStaleDataUX,
|
||||
iOSStaleDataUX,
|
||||
WebStaleDataUX,
|
||||
STALE_DATA_CONFIG,
|
||||
I18N_KEYS
|
||||
};
|
||||
Reference in New Issue
Block a user