/** * 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 { // 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(); async get(key: string): Promise { return this.storage.get(key); } async set(key: string, value: any): Promise { this.storage.set(key, value); } async delete(key: string): Promise { this.storage.delete(key); } async exists(key: string): Promise { return this.storage.has(key); } } // Mock authentication manager class MockAuthManager { private token = 'mock_jwt_token'; async getCurrentToken(): Promise { return this.token; } async refreshToken(): Promise { this.token = `mock_jwt_token_${Date.now()}`; return this.token; } async validateToken(token: string): Promise { 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( request: GenericPollingRequest ): Promise> { 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( config: PollingScheduleConfig ): Promise { 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 { 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 = { 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 = { 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 };