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!,
|
|
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 };
|
|
|