Browse Source

Merge branch 'master' of ssh://173.199.124.46:222/trent_larson/daily-notification-plugin

master
Matthew Raymer 4 days ago
parent
commit
fb47f3e717
  1. 183
      doc/STARRED_PROJECTS_POLLING_IMPLEMENTATION.md

183
doc/STARRED_PROJECTS_POLLING_IMPLEMENTATION.md

@ -367,7 +367,7 @@ interface PreviousClaim {
"version": "1.0.0" "version": "1.0.0"
}, },
"previousClaim": { "previousClaim": {
"jwtId": "1703980800_xyz789_ghi01234", "jwtId": "1703980800_xyz789_0badf00d",
"claimType": "project_update", "claimType": "project_update",
"claimData": { "claimData": {
"status": "in_progress", "status": "in_progress",
@ -384,7 +384,7 @@ interface PreviousClaim {
"hitLimit": false, "hitLimit": false,
"pagination": { "pagination": {
"hasMore": true, "hasMore": true,
"nextAfterId": "1704153600_mno345_pqr67890" "nextAfterId": "1704153600_mno345_0badf00d"
} }
} }
``` ```
@ -525,7 +525,7 @@ function compareJwtIds(a: string, b: string): number {
return a.localeCompare(b); return a.localeCompare(b);
} }
// Example: "1704067200_abc123_def45678" < "1704153600_xyz789_ghi01234" // Example: "1704067200_abc123_def45678" < "1704153600_xyz789_0badf00d"
``` ```
**Eventual Consistency Bounds**: **Eventual Consistency Bounds**:
@ -553,7 +553,7 @@ X-Idempotency-Key: {uuid}
**Request Body**: **Request Body**:
```json ```json
{ {
"acknowledgedJwtIds": ["1704067200_abc123_def45678", "1704153600_mno345_pqr67890"], "acknowledgedJwtIds": ["1704067200_abc123_def45678", "1704153600_mno345_0badf00d"],
"acknowledgedAt": "2025-01-01T12:00:00Z", "acknowledgedAt": "2025-01-01T12:00:00Z",
"clientVersion": "TimeSafari-DailyNotificationPlugin/1.0.0" "clientVersion": "TimeSafari-DailyNotificationPlugin/1.0.0"
} }
@ -1128,7 +1128,9 @@ document.addEventListener('visibilitychange', () => {
const lastPoll = localStorage.getItem('lastPollTimestamp'); const lastPoll = localStorage.getItem('lastPollTimestamp');
const now = Date.now(); const now = Date.now();
if (now - parseInt(lastPoll) > 3600000) { // 1 hour if (now - parseInt(lastPoll) > 3600000) { // 1 hour
pollStarredProjects(); pollStarredProjects().then(() => {
localStorage.setItem('lastPollTimestamp', now.toString());
});
} }
} }
}); });
@ -1147,13 +1149,15 @@ document.addEventListener('visibilitychange', () => {
[Make API Call] ← [Valid Config] ← [Has Starred Projects] ← [Has Last Ack ID] [Make API Call] ← [Valid Config] ← [Has Starred Projects] ← [Has Last Ack ID]
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
[Network Error] [Parse Response] [Process Results] [Update Watermark] [Network Error] [Parse Response] [Process Results] [Generate Notifications]
↓ ↓ ↓ ↓
[Retry Logic] [Schedule Delivery] [Success] [Acknowledge Delivery]
↓ ↓ ↓ ↓
[Exponential Backoff] [Acknowledge Delivery] [End] [Update Watermark]
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
[Retry Logic] [Generate Notifications] [Success] [Commit State] [Max Retries] [Update Watermark] [End] [Commit State]
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
[Exponential Backoff] [Schedule Delivery] [End] [End] [End] [Commit State] [End] [End]
↓ ↓
[Max Retries] [End]
[End] [End]
``` ```
@ -1164,8 +1168,8 @@ document.addEventListener('visibilitychange', () => {
3. **Check Last Ack ID**: If `lastAckedStarredPlanChangesJwtId` is null, run Bootstrap Watermark; then continue 3. **Check Last Ack ID**: If `lastAckedStarredPlanChangesJwtId` is null, run Bootstrap Watermark; then continue
4. **Make API Call**: Execute authenticated POST request 4. **Make API Call**: Execute authenticated POST request
5. **Process Results**: Parse response and extract change count 5. **Process Results**: Parse response and extract change count
6. **Update Watermark**: Advance watermark only after successful delivery AND acknowledgment 6. **Generate Notifications**: Create user notifications for changes
7. **Generate Notifications**: Create user notifications for changes 7. **Update Watermark**: Advance watermark only after successful delivery AND acknowledgment
#### Watermark Bootstrap Path #### Watermark Bootstrap Path
@ -1390,10 +1394,10 @@ class OutboxPressureManager {
**Atomic Transaction Pattern**: **Atomic Transaction Pattern**:
```sql ```sql
-- Phase 1: Atomic commit of watermark + outbox -- Phase 1: Atomic commit of outbox only (watermark stays unchanged)
BEGIN TRANSACTION; BEGIN TRANSACTION;
INSERT INTO notification_outbox (jwt_id, content) VALUES (?, ?); INSERT INTO notification_outbox (jwt_id, content) VALUES (?, ?);
UPDATE settings SET lastAckedStarredPlanChangesJwtId = ? WHERE accountDid = ?; -- Do NOT update watermark here - it advances only after delivery + acknowledgment
COMMIT; COMMIT;
``` ```
@ -1471,7 +1475,7 @@ async function processPollingResults(results: PollingResult[]): Promise<void> {
} }
// 3. Dispatcher delivers notifications // 3. Dispatcher delivers notifications
await notificationDispatcher.processOutbox(); const { deliveredJwtIds, latestJwtId } = await notificationDispatcher.processOutbox();
// 4. After successful delivery, call acknowledgment endpoint // 4. After successful delivery, call acknowledgment endpoint
await acknowledgeDeliveredNotifications(deliveredJwtIds); await acknowledgeDeliveredNotifications(deliveredJwtIds);
@ -1507,7 +1511,7 @@ UPDATE notification_outbox SET delivered_at = datetime('now'), acknowledged_at =
``` ```
**Recovery Rules**: **Recovery Rules**:
- **Crash After Watermark Update**: On restart, check `notification_pending` table for uncommitted notifications - **Crash After Watermark Update**: On restart, check `notification_outbox` table for uncommitted notifications
- **Crash Before Watermark Update**: Safe to retry - no state change occurred - **Crash Before Watermark Update**: Safe to retry - no state change occurred
- **Partial Notification Failure**: Rollback watermark update, retry entire transaction - **Partial Notification Failure**: Rollback watermark update, retry entire transaction
- **Acknowledgment Endpoint**: Call `POST /api/v2/plans/acknowledge` after successful notification delivery - **Acknowledgment Endpoint**: Call `POST /api/v2/plans/acknowledge` after successful notification delivery
@ -1515,13 +1519,13 @@ UPDATE notification_outbox SET delivered_at = datetime('now'), acknowledged_at =
**Recovery Implementation**: **Recovery Implementation**:
```typescript ```typescript
async function recoverPendingNotifications(): Promise<void> { async function recoverPendingNotifications(): Promise<void> {
const pending = await db.query('SELECT * FROM notification_pending WHERE created_at < ?', const pending = await db.query('SELECT * FROM notification_outbox WHERE created_at < ?',
[Date.now() - 300000]); // 5 minutes ago [Date.now() - 300000]); // 5 minutes ago
for (const notification of pending) { for (const notification of pending) {
try { try {
await scheduleNotification(notification.content); await scheduleNotification(notification.content);
await db.query('DELETE FROM notification_pending WHERE id = ?', [notification.id]); await db.query('DELETE FROM notification_outbox WHERE id = ?', [notification.id]);
} catch (error) { } catch (error) {
// Log error, will retry on next recovery cycle // Log error, will retry on next recovery cycle
console.error('Failed to recover notification:', error); console.error('Failed to recover notification:', error);
@ -1578,7 +1582,7 @@ Body: "You have {count} new updates in your starred projects"
**Deep Link Routes**: **Deep Link Routes**:
``` ```
timesafari://projects/updates?jwtIds=1704067200_abc123_def45678,1704153600_mno345_pqr67890 timesafari://projects/updates?jwtIds=1704067200_abc123_def45678,1704153600_mno345_0badf00d
timesafari://projects/{projectId}/details?jwtId=1704067200_abc123_def45678 timesafari://projects/{projectId}/details?jwtId=1704067200_abc123_def45678
timesafari://notifications/starred-projects timesafari://notifications/starred-projects
timesafari://projects/updates?shortlink=abc123def456789 timesafari://projects/updates?shortlink=abc123def456789
@ -1613,7 +1617,7 @@ function validateDeepLinkParams(params: any): DeepLinkValidation {
```typescript ```typescript
// Server generates shortlink for large result sets // Server generates shortlink for large result sets
const shortlink = await generateShortlink({ const shortlink = await generateShortlink({
jwtIds: ['1704067200_abc123_def45678', '1704153600_mno345_pqr67890', /* ... 50 more */], jwtIds: ['1704067200_abc123_def45678', '1704153600_mno345_0badf00d', /* ... 50 more */],
expiresAt: Date.now() + 86400000 // 24 hours expiresAt: Date.now() + 86400000 // 24 hours
}); });
@ -2004,125 +2008,6 @@ const PII_REDACTION_PATTERNS = [
### Testing Artifacts ### Testing Artifacts
#### Mock Fixtures
**Empty Response**:
```json
{
"data": [],
"hitLimit": false,
"pagination": {
"hasMore": false,
"nextAfterId": null
}
}
```
**Small Response (3 items)**:
```json
{
"data": [
{
"planSummary": {
"jwtId": "1704067200_abc123_def45678",
"handleId": "test_project_1",
"name": "Test Project 1",
"description": "First test project"
},
"previousClaim": {
"jwtId": "1703980800_xyz789_ghi01234",
"claimType": "project_update"
}
},
{
"planSummary": {
"jwtId": "1704153600_mno345_pqr67890",
"handleId": "test_project_2",
"name": "Test Project 2",
"description": "Second test project"
},
"previousClaim": {
"jwtId": "1704067200_stu901_vwx23456",
"claimType": "project_update"
}
},
{
"planSummary": {
"jwtId": "1704240000_new123_0badf00d",
"handleId": "test_project_3",
"name": "Test Project 3",
"description": "Third test project"
},
"previousClaim": {
"jwtId": "1704153600_old456_1cafebad",
"claimType": "project_update"
}
}
],
"hitLimit": false,
"pagination": {
"hasMore": false,
"nextAfterId": null
}
}
```
**Paginated Response**:
```json
{
"data": [...], // 100 items
"hitLimit": true,
"pagination": {
"hasMore": true,
"nextAfterId": "1704153600_mno345_pqr67890"
}
}
```
**Rate Limited Response (canonical format)**:
```json
{
"error": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED",
"retryAfter": 60,
"details": {
"limit": 100,
"window": "1m",
"remaining": 0,
"resetAt": "2024-01-01T12:01:00Z"
},
"requestId": "req_jkl012"
}
```
**Contract Tests**:
```typescript
// JWT ID comparison helper
function compareJwtIds(a: string, b: string): number {
return a.localeCompare(b); // Lexicographic comparison for fixed-width format
}
describe('StarredProjectsPolling Contract Tests', () => {
test('should maintain JWT ID ordering', () => {
const response = mockPaginatedResponse();
const jwtIds = response.data.map(item => item.planSummary.jwtId);
const sortedIds = [...jwtIds].sort(compareJwtIds);
expect(jwtIds).toEqual(sortedIds);
});
test('should handle watermark movement correctly', () => {
const initialWatermark = '1704067200_abc123_def45678';
const response = mockSmallResponse();
const newWatermark = getNewWatermark(response);
expect(compareJwtIds(newWatermark, initialWatermark)).toBeGreaterThan(0);
});
test('should respect pagination limits', () => {
const response = mockPaginatedResponse();
expect(response.data.length).toBeLessThanOrEqual(100);
});
});
```
#### Testing Fixtures & SLAs #### Testing Fixtures & SLAs
@ -2153,16 +2038,16 @@ describe('StarredProjectsPolling Contract Tests', () => {
"issuerDid": "did:key:test_issuer_1", "issuerDid": "did:key:test_issuer_1",
"agentDid": "did:key:test_agent_1", "agentDid": "did:key:test_agent_1",
"locLat": 40.7128, "locLat": 40.7128,
"locLon": -74.0060, "locLon": -74.0060
}, },
"previousClaim": { "previousClaim": {
"jwtId": "1703980800_xyz789_ghi01234", "jwtId": "1703980800_xyz789_0badf00d",
"claimType": "project_update" "claimType": "project_update"
} }
}, },
{ {
"planSummary": { "planSummary": {
"jwtId": "1704153600_mno345_pqr67890", "jwtId": "1704153600_mno345_0badf00d",
"handleId": "test_project_2", "handleId": "test_project_2",
"name": "Test Project 2", "name": "Test Project 2",
"description": "Second test project", "description": "Second test project",
@ -2172,7 +2057,7 @@ describe('StarredProjectsPolling Contract Tests', () => {
"locLon": null "locLon": null
}, },
"previousClaim": { "previousClaim": {
"jwtId": "1704067200_stu901_vwx23456", "jwtId": "1704067200_stu901_1cafebad",
"claimType": "project_update" "claimType": "project_update"
} }
}, },
@ -2188,7 +2073,7 @@ describe('StarredProjectsPolling Contract Tests', () => {
"locLon": -122.4194 "locLon": -122.4194
}, },
"previousClaim": { "previousClaim": {
"jwtId": "1704153600_old456_1cafebad", "jwtId": "1704153600_old456_0badf00d",
"claimType": "project_update" "claimType": "project_update"
} }
} }
@ -2205,12 +2090,12 @@ describe('StarredProjectsPolling Contract Tests', () => {
```json ```json
{ {
"data": [ "data": [
// ... 100 items with jwtIds from "1704067200_abc123_def45678" to "1704153600_xyz789_ghi01234" // ... 100 items with jwtIds from "1704067200_abc123_def45678" to "1704153600_xyz789_0badf00d"
], ],
"hitLimit": true, "hitLimit": true,
"pagination": { "pagination": {
"hasMore": true, "hasMore": true,
"nextAfterId": "1704240000_new123_0badf00d" "nextAfterId": "1704240000_new123_1cafebad"
} }
} }
``` ```
@ -2245,7 +2130,7 @@ describe('StarredProjectsPolling Contract Tests', () => {
"locLon": null "locLon": null
}, },
"previousClaim": { "previousClaim": {
"jwtId": "1703980800_xyz789_ghi01234", "jwtId": "1703980800_xyz789_0badf00d",
"claimType": "project_update" "claimType": "project_update"
} }
} }
@ -2264,7 +2149,7 @@ describe('StarredProjectsPolling Contract Tests', () => {
"data": [ "data": [
{ {
"planSummary": { "planSummary": {
"jwtId": "1704153600_mno345_pqr67890", "jwtId": "1704153600_mno345_0badf00d",
"handleId": "test_project_2", "handleId": "test_project_2",
"locLat": null, "locLat": null,
"locLon": null "locLon": null
@ -2275,7 +2160,7 @@ describe('StarredProjectsPolling Contract Tests', () => {
"jwtId": "1704067200_abc123_def45678", "jwtId": "1704067200_abc123_def45678",
"handleId": "test_project_1", "handleId": "test_project_1",
"locLat": 40.7128, "locLat": 40.7128,
"locLon": -74.0060, "locLon": -74.0060
} }
} }
], ],

Loading…
Cancel
Save