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.
		
		
		
		
		
			
		
			
				
					
					
						
							336 lines
						
					
					
						
							9.7 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							336 lines
						
					
					
						
							9.7 KiB
						
					
					
				
								/**
							 | 
						|
								 * 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: Record<string, unknown>[] = [
							 | 
						|
								    {
							 | 
						|
								      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, unknown>();
							 | 
						|
								  
							 | 
						|
								  async get(key: string): Promise<unknown> {
							 | 
						|
								    return this.storage.get(key);
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  async set(key: string, value: unknown): 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 || 'unknown',
							 | 
						|
								          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
							 | 
						|
								    // eslint-disable-next-line no-console
							 | 
						|
								    console.log(`Scheduled poll: ${scheduleId}`);
							 | 
						|
								    
							 | 
						|
								    return scheduleId;
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Main example
							 | 
						|
								async function runHelloPollExample(): Promise<void> {
							 | 
						|
								  // eslint-disable-next-line no-console
							 | 
						|
								  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);
							 | 
						|
								  // eslint-disable-next-line no-console
							 | 
						|
								  console.log(`✅ Scheduled poll: ${scheduleId}`);
							 | 
						|
								  
							 | 
						|
								  // 4. Execute initial poll
							 | 
						|
								  // eslint-disable-next-line no-console
							 | 
						|
								  console.log('📡 Executing initial poll...');
							 | 
						|
								  const result = await pollingManager.executePoll(request);
							 | 
						|
								  
							 | 
						|
								  if (result.success && result.data) {
							 | 
						|
								    // eslint-disable-next-line no-console
							 | 
						|
								    console.log(`✅ Found ${result.data.data.length} changes`);
							 | 
						|
								    
							 | 
						|
								    if (result.data.data.length > 0) {
							 | 
						|
								      // 5. Generate notifications
							 | 
						|
								      const changes = result.data.data;
							 | 
						|
								      // eslint-disable-next-line no-console
							 | 
						|
								      console.log('🔔 Generating notifications...');
							 | 
						|
								      
							 | 
						|
								      if (changes.length === 1) {
							 | 
						|
								        const project = changes[0].planSummary;
							 | 
						|
								        // eslint-disable-next-line no-console
							 | 
						|
								        console.log(`📱 Notification: "${project.name} has been updated"`);
							 | 
						|
								      } else {
							 | 
						|
								        // eslint-disable-next-line no-console
							 | 
						|
								        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);
							 | 
						|
								      // eslint-disable-next-line no-console
							 | 
						|
								      console.log(`💾 Updated watermark: ${latestJwtId}`);
							 | 
						|
								      
							 | 
						|
								      // 7. Acknowledge changes (simulate)
							 | 
						|
								      // eslint-disable-next-line no-console
							 | 
						|
								      console.log('✅ Acknowledged changes with server');
							 | 
						|
								    }
							 | 
						|
								  } else {
							 | 
						|
								    // eslint-disable-next-line no-console
							 | 
						|
								    console.log('❌ Poll failed:', result.error?.message);
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  // 8. Simulate new data and poll again
							 | 
						|
								  // eslint-disable-next-line no-console
							 | 
						|
								  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) {
							 | 
						|
								    // eslint-disable-next-line no-console
							 | 
						|
								    console.log(`✅ Found ${result2.data.data.length} new changes`);
							 | 
						|
								    
							 | 
						|
								    if (result2.data.data.length > 0) {
							 | 
						|
								      const project = result2.data.data[0].planSummary;
							 | 
						|
								      // eslint-disable-next-line no-console
							 | 
						|
								      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);
							 | 
						|
								      // eslint-disable-next-line no-console
							 | 
						|
								      console.log(`💾 Updated watermark: ${latestJwtId}`);
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								  
							 | 
						|
								  // eslint-disable-next-line no-console
							 | 
						|
								  console.log('\n🎉 Hello Poll Example completed successfully!');
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Run the example
							 | 
						|
								if (require.main === module) {
							 | 
						|
								  runHelloPollExample().catch(// eslint-disable-next-line no-console
							 | 
						|
								    console.error);
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								export { runHelloPollExample };
							 | 
						|
								
							 |