Social Annotations in the Atmosphere
15
fork

Configure Feed

Select the types of activity you want to include in your feed.

arcitecture

+763
+763
STORAGE_FIRST_ARCHITECTURE.md
··· 1 + # Storage-First Architecture Implementation Plan 2 + 3 + ## Overview 4 + 5 + This architecture eliminates fragile message passing by using `browser.storage.local` as the single source of truth for annotations and comments. Components read from and write to storage, with the background worker responsible for syncing with the PDS (Personal Data Server). 6 + 7 + ## Core Principles 8 + 9 + 1. **Storage as truth**: `storage.local` is the cache, PDS is the source 10 + 2. **No broadcasts**: Components don't send messages to each other about data changes 11 + 3. **Storage watchers**: Components react to `storage.onChanged` events 12 + 4. **Request-response only**: Sidepanel queries content script for ephemeral state (selection) 13 + 5. **Sync on write**: After PDS writes, trigger background sync to refresh cache 14 + 15 + ## Architecture Diagram 16 + 17 + ``` 18 + ┌─────────────────────────────────────────────────────────┐ 19 + │ PDS (AT Protocol) │ 20 + │ - Source of truth for annotations/comments │ 21 + └────────────┬────────────────────────────▲───────────────┘ 22 + │ sync │ write 23 + │ │ 24 + ┌────────────▼────────────────────────────┴───────────────┐ 25 + │ Background Worker │ 26 + │ - Syncs PDS → storage.local on startup │ 27 + │ - Listens for SYNC_CACHE message │ 28 + │ - Re-syncs on request │ 29 + └────────────┬────────────────────────────────────────────┘ 30 + │ write 31 + 32 + ┌────────────▼────────────────────────────────────────────┐ 33 + │ browser.storage.local │ 34 + │ { │ 35 + │ annotations: Annotation[], │ 36 + │ comments: Comment[], │ 37 + │ lastSync: timestamp │ 38 + │ } │ 39 + └────────┬───────────────────────┬────────────────────────┘ 40 + │ read + watch │ read + watch 41 + │ │ 42 + ┌────────▼─────────┐ ┌────────▼──────────────────────┐ 43 + │ Content Script │ │ Sidepanel │ 44 + │ - Filter by URL │ │ - Display annotations │ 45 + │ - Render │ │ - Create/edit via PDS │ 46 + │ - Watch changes │ │ - Request sync after write │ 47 + └──────────────────┘ └───────────────────────────────┘ 48 + 49 + │ GET_STATE message 50 + │ (url, selection) 51 + └──────────────────────────────────────────────┘ 52 + ``` 53 + 54 + ## Component Responsibilities 55 + 56 + ### Background Worker (`entrypoints/background.ts`) 57 + 58 + **Purpose**: Sync coordinator between PDS and storage.local 59 + 60 + **Responsibilities**: 61 + - Sync annotations/comments from PDS to storage.local on extension startup 62 + - Listen for `SYNC_CACHE` messages and re-sync 63 + - Register context menu 64 + - (Optional) Listen for `tabs.onUpdated` to detect URL changes and pre-fetch 65 + 66 + **State**: None (stateless) 67 + 68 + **API Surface**: 69 + ```typescript 70 + // Message handler 71 + onMessage: 'SYNC_CACHE' → triggers syncFromPDS() 72 + 73 + // Functions 74 + syncFromPDS(): Promise<void> 75 + - loadSession() 76 + - listAnnotations() 77 + - listComments() 78 + - storage.local.set({ annotations, comments, lastSync }) 79 + ``` 80 + 81 + **Implementation Details**: 82 + ```typescript 83 + // Sync on startup 84 + browser.runtime.onStartup.addListener(async () => { 85 + await syncFromPDS(); 86 + }); 87 + 88 + // Sync on install (first run) 89 + browser.runtime.onInstalled.addListener(async () => { 90 + await syncFromPDS(); 91 + }); 92 + 93 + // Sync on request 94 + browser.runtime.onMessage.addListener((message) => { 95 + if (message.type === 'SYNC_CACHE') { 96 + syncFromPDS().catch(err => { 97 + console.error('[background] Sync failed:', err); 98 + }); 99 + } 100 + }); 101 + 102 + async function syncFromPDS() { 103 + const session = await loadSession(); 104 + if (!session) { 105 + console.log('[background] No session, skipping sync'); 106 + return; 107 + } 108 + 109 + console.log('[background] Syncing from PDS...'); 110 + 111 + try { 112 + const annotations = await listAnnotations(); 113 + const comments = await listComments(); 114 + 115 + await browser.storage.local.set({ 116 + annotations, 117 + comments, 118 + lastSync: Date.now() 119 + }); 120 + 121 + console.log('[background] Sync complete:', { 122 + annotations: annotations.length, 123 + comments: comments.length 124 + }); 125 + } catch (error) { 126 + console.error('[background] Sync error:', error); 127 + throw error; 128 + } 129 + } 130 + ``` 131 + 132 + --- 133 + 134 + ### Content Script (`entrypoints/content.ts`) 135 + 136 + **Purpose**: Render highlights on page, track selection 137 + 138 + **Responsibilities**: 139 + - Load annotations from storage.local on page load 140 + - Filter by current URL 141 + - Apply highlights to DOM 142 + - Watch `storage.onChanged` to re-render when cache updates 143 + - Track user text selection (mouseup) 144 + - Respond to `GET_STATE` requests from sidepanel 145 + 146 + **State**: 147 + ```typescript 148 + let currentSelection: { text: string; selectors: Selector[] } | null = null; 149 + let currentUrl: string = window.location.href; 150 + ``` 151 + 152 + **API Surface**: 153 + ```typescript 154 + // Message handler 155 + onMessage: 'GET_STATE' → response: { url, selection } 156 + 157 + // Functions 158 + loadAndRenderHighlights(): Promise<void> 159 + trackSelection(): void 160 + ``` 161 + 162 + **Implementation Details**: 163 + ```typescript 164 + // On page load 165 + (async function init() { 166 + currentUrl = window.location.href; 167 + await loadAndRenderHighlights(); 168 + })(); 169 + 170 + async function loadAndRenderHighlights() { 171 + const { annotations } = await browser.storage.local.get('annotations'); 172 + 173 + if (!annotations || annotations.length === 0) { 174 + console.log('[content] No annotations in cache'); 175 + return; 176 + } 177 + 178 + // Filter by current URL 179 + const pageAnnotations = annotations.filter( 180 + (ann: Annotation) => ann.target[0]?.source === currentUrl 181 + ); 182 + 183 + console.log(`[content] Found ${pageAnnotations.length} annotations for ${currentUrl}`); 184 + 185 + if (pageAnnotations.length > 0) { 186 + applyHighlights(pageAnnotations); 187 + } 188 + } 189 + 190 + // Watch for storage changes 191 + browser.storage.onChanged.addListener((changes, area) => { 192 + if (area === 'local' && changes.annotations) { 193 + console.log('[content] Annotations cache updated, re-rendering'); 194 + loadAndRenderHighlights(); 195 + } 196 + }); 197 + 198 + // Track selection 199 + document.addEventListener('mouseup', () => { 200 + const selection = window.getSelection(); 201 + if (selection && selection.toString().trim().length > 0) { 202 + const text = selection.toString().trim(); 203 + const root = document.querySelector('main') || 204 + document.querySelector('article') || 205 + document.body; 206 + const selectors = generateSelectors(selection, root); 207 + 208 + currentSelection = { text, selectors }; 209 + } else { 210 + currentSelection = null; 211 + } 212 + }); 213 + 214 + // Respond to state requests 215 + browser.runtime.onMessage.addListener((message, sender, sendResponse) => { 216 + if (message.type === 'GET_STATE') { 217 + sendResponse({ 218 + url: currentUrl, 219 + selection: currentSelection 220 + }); 221 + return true; 222 + } 223 + }); 224 + ``` 225 + 226 + **Edge Cases**: 227 + - URL changes (SPA navigation): Need to detect and update `currentUrl` 228 + - Option A: `setInterval` polling (simple but inelegant) 229 + - Option B: Listen to `popstate` and `pushstate` events 230 + - Option C: MutationObserver on `<title>` changes (proxy for navigation) 231 + 232 + **Recommendation**: Listen to history events 233 + ```typescript 234 + // Detect SPA navigation 235 + let lastUrl = window.location.href; 236 + 237 + // Native navigation events 238 + window.addEventListener('popstate', handleUrlChange); 239 + 240 + // Intercept pushState/replaceState 241 + const originalPushState = history.pushState; 242 + history.pushState = function(...args) { 243 + originalPushState.apply(this, args); 244 + handleUrlChange(); 245 + }; 246 + 247 + const originalReplaceState = history.replaceState; 248 + history.replaceState = function(...args) { 249 + originalReplaceState.apply(this, args); 250 + handleUrlChange(); 251 + }; 252 + 253 + function handleUrlChange() { 254 + const newUrl = window.location.href; 255 + if (newUrl !== lastUrl) { 256 + console.log('[content] URL changed:', lastUrl, '→', newUrl); 257 + lastUrl = newUrl; 258 + currentUrl = newUrl; 259 + currentSelection = null; 260 + clearHighlights(); 261 + loadAndRenderHighlights(); 262 + } 263 + } 264 + ``` 265 + 266 + --- 267 + 268 + ### Sidepanel (`entrypoints/sidepanel/main.ts`) 269 + 270 + **Purpose**: User interface for viewing/creating annotations 271 + 272 + **Responsibilities**: 273 + - Display annotations/comments from storage.local 274 + - Track active tab (via `tabs.onActivated`) 275 + - Query content script for selection state via `GET_STATE` 276 + - Create/edit annotations by writing to PDS 277 + - Request background sync after PDS writes 278 + - Handle login/logout 279 + 280 + **State**: 281 + ```typescript 282 + let currentTabId: number | null = null; 283 + let currentUrl: string = ''; 284 + let currentSelection: { text: string; selectors: Selector[] } | null = null; 285 + let pageAnnotations: Annotation[] = []; 286 + let allComments: Comment[] = []; 287 + ``` 288 + 289 + **API Surface**: 290 + ```typescript 291 + // Functions 292 + refreshActiveTab(): Promise<void> 293 + loadAnnotationsForUrl(url: string): Promise<void> 294 + createAndSyncAnnotation(annotation: Annotation): Promise<void> 295 + ``` 296 + 297 + **Implementation Details**: 298 + ```typescript 299 + // On open 300 + (async function init() { 301 + await refreshActiveTab(); 302 + })(); 303 + 304 + async function refreshActiveTab() { 305 + // Get active tab 306 + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); 307 + const activeTab = tabs[0]; 308 + 309 + if (!activeTab?.id) { 310 + console.error('[sidepanel] No active tab'); 311 + return; 312 + } 313 + 314 + currentTabId = activeTab.id; 315 + 316 + // Get state from content script 317 + try { 318 + const response = await browser.tabs.sendMessage(currentTabId, { 319 + type: 'GET_STATE' 320 + }); 321 + 322 + currentUrl = response.url; 323 + currentSelection = response.selection; 324 + 325 + // Update UI 326 + updateSelectionUI(); 327 + 328 + // Load annotations for this URL 329 + await loadAnnotationsForUrl(currentUrl); 330 + } catch (error) { 331 + console.error('[sidepanel] Failed to get state:', error); 332 + // Content script might not be ready on restricted pages 333 + } 334 + } 335 + 336 + async function loadAnnotationsForUrl(url: string) { 337 + const { annotations, comments } = await browser.storage.local.get([ 338 + 'annotations', 339 + 'comments' 340 + ]); 341 + 342 + // Filter by URL 343 + pageAnnotations = (annotations || []).filter( 344 + (ann: Annotation) => ann.target[0]?.source === url 345 + ); 346 + 347 + allComments = comments || []; 348 + 349 + renderAnnotations(); 350 + } 351 + 352 + // Watch for tab changes 353 + browser.tabs.onActivated.addListener(({ tabId }) => { 354 + if (tabId !== currentTabId) { 355 + console.log('[sidepanel] Active tab changed:', tabId); 356 + refreshActiveTab(); 357 + } 358 + }); 359 + 360 + // Watch for URL changes in active tab 361 + browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 362 + if (tabId === currentTabId && changeInfo.url) { 363 + console.log('[sidepanel] Active tab URL changed:', changeInfo.url); 364 + currentUrl = changeInfo.url; 365 + currentSelection = null; 366 + updateSelectionUI(); 367 + loadAnnotationsForUrl(currentUrl); 368 + } 369 + }); 370 + 371 + // Watch for storage changes 372 + browser.storage.onChanged.addListener((changes, area) => { 373 + if (area === 'local' && (changes.annotations || changes.comments)) { 374 + console.log('[sidepanel] Cache updated, reloading for current URL'); 375 + loadAnnotationsForUrl(currentUrl); 376 + } 377 + }); 378 + 379 + // Create annotation 380 + async function createAndSyncAnnotation(annotation: Annotation) { 381 + try { 382 + // Save to PDS 383 + const saved = await createAnnotation(annotation); 384 + console.log('[sidepanel] Saved to PDS:', saved.uri); 385 + 386 + // Request background sync 387 + browser.runtime.sendMessage({ type: 'SYNC_CACHE' }); 388 + 389 + // Clear form 390 + currentSelection = null; 391 + updateSelectionUI(); 392 + 393 + // Note: UI will update automatically via storage.onChanged 394 + } catch (error) { 395 + console.error('[sidepanel] Failed to create annotation:', error); 396 + alert('Failed to save annotation'); 397 + } 398 + } 399 + ``` 400 + 401 + **Edge Cases**: 402 + - Tab closes while sidepanel open: `tabs.onRemoved` listener 403 + - Restricted pages (chrome://, PDFs): `GET_STATE` fails, show friendly message 404 + - Multiple windows: Only track active tab in current window 405 + 406 + --- 407 + 408 + ## Storage Schema 409 + 410 + ### storage.local 411 + 412 + ```typescript 413 + interface StorageSchema { 414 + annotations: Annotation[]; 415 + comments: Comment[]; 416 + lastSync: number; // timestamp 417 + 'synthesis-oauth:session': OAuthSession; // Already exists 418 + } 419 + ``` 420 + 421 + **Size Considerations**: 422 + - storage.local quota: 10MB (unlimitedStorage permission increases this) 423 + - Typical annotation: ~500 bytes 424 + - 10MB / 500 bytes = ~20,000 annotations 425 + - Sufficient for most users 426 + 427 + **Optimization (future)**: 428 + - Index by URL for faster filtering: `Map<url, Annotation[]>` 429 + - Store as: `{ annotationsByUrl: { "https://...": [...] } }` 430 + 431 + --- 432 + 433 + ## Message Protocol 434 + 435 + ### Sidepanel → Content Script 436 + 437 + **GET_STATE** 438 + ```typescript 439 + // Request 440 + { type: 'GET_STATE' } 441 + 442 + // Response 443 + { 444 + url: string; 445 + selection: { text: string; selectors: Selector[] } | null; 446 + } 447 + ``` 448 + 449 + ### Sidepanel → Background 450 + 451 + **SYNC_CACHE** 452 + ```typescript 453 + { type: 'SYNC_CACHE' } 454 + 455 + // No response expected (fire and forget) 456 + ``` 457 + 458 + ### No Other Messages 459 + 460 + All other communication happens via `storage.onChanged` events. 461 + 462 + --- 463 + 464 + ## Implementation Steps 465 + 466 + ### Phase 1: Background Sync 467 + 468 + 1. ✅ Update `background.ts`: 469 + - Add `syncFromPDS()` function 470 + - Add startup/install listeners 471 + - Add `SYNC_CACHE` message handler 472 + 473 + 2. ✅ Move PDS functions to background context: 474 + - Ensure `loadSession()`, `listAnnotations()`, `listComments()` work in background 475 + - Handle errors gracefully (network failures, auth errors) 476 + 477 + ### Phase 2: Content Script Storage Integration 478 + 479 + 1. ✅ Update `content.ts`: 480 + - Remove `SELECTION_CHANGED` broadcast 481 + - Add `loadAndRenderHighlights()` on page load 482 + - Add `storage.onChanged` listener 483 + - Keep `GET_STATE` message handler 484 + - Add URL change detection 485 + 486 + 2. ✅ Test: 487 + - Highlights appear on page load 488 + - Highlights update when storage changes 489 + - Selection still tracked locally 490 + 491 + ### Phase 3: Sidepanel Storage Integration 492 + 493 + 1. ✅ Update `sidepanel/main.ts`: 494 + - Remove `GET_SELECTION` polling on open 495 + - Add `refreshActiveTab()` on open 496 + - Add `tabs.onActivated` listener 497 + - Add `tabs.onUpdated` listener 498 + - Add `storage.onChanged` listener 499 + - Update `saveAnnotation` to call PDS + `SYNC_CACHE` 500 + 501 + 2. ✅ Test: 502 + - Tab switching updates UI 503 + - URL changes update UI 504 + - Creating annotation triggers sync 505 + - UI updates automatically after sync 506 + 507 + ### Phase 4: Clean Up 508 + 509 + 1. ✅ Remove deprecated code: 510 + - Remove `SELECTION_CHANGED` broadcast handling 511 + - Remove old `GET_SELECTION` handler (replaced by `GET_STATE`) 512 + - Remove legacy local storage code 513 + 514 + 2. ✅ Remove from `STATE_REFACTOR_PLAN.md`: 515 + - Port-based communication (not needed) 516 + - Background state cache (using storage.local instead) 517 + - `storage.session` (not needed) 518 + 519 + ### Phase 5: Testing 520 + 521 + See manual testing checklist below. 522 + 523 + --- 524 + 525 + ## Edge Cases & Handling 526 + 527 + ### 1. Content Script Not Ready 528 + 529 + **Scenario**: Sidepanel opens before content script loads 530 + 531 + **Handling**: 532 + ```typescript 533 + try { 534 + const response = await browser.tabs.sendMessage(tabId, { type: 'GET_STATE' }); 535 + } catch (error) { 536 + console.error('[sidepanel] Content script not available:', error); 537 + // Show message: "This page doesn't support annotations" 538 + // Still load annotations from cache to show in list 539 + } 540 + ``` 541 + 542 + ### 2. Restricted Pages 543 + 544 + **Scenario**: chrome://, PDFs, extension pages don't allow content scripts 545 + 546 + **Handling**: Same as above - graceful degradation 547 + 548 + ### 3. URL Normalization 549 + 550 + **Scenario**: `example.com/page` vs `example.com/page/` vs `example.com/page#hash` 551 + 552 + **Handling**: 553 + ```typescript 554 + function normalizeUrl(url: string): string { 555 + try { 556 + const parsed = new URL(url); 557 + // Remove fragment 558 + parsed.hash = ''; 559 + // Remove trailing slash 560 + let path = parsed.pathname; 561 + if (path.endsWith('/') && path !== '/') { 562 + path = path.slice(0, -1); 563 + } 564 + parsed.pathname = path; 565 + return parsed.toString(); 566 + } catch { 567 + return url; 568 + } 569 + } 570 + ``` 571 + 572 + Apply normalization when: 573 + - Storing annotation source 574 + - Filtering annotations by URL 575 + 576 + ### 4. Sync Conflicts 577 + 578 + **Scenario**: User creates annotation in Tab A while sync is in progress 579 + 580 + **Handling**: Last-write-wins (PDS is source of truth) 581 + - Sidepanel writes to PDS immediately 582 + - Background sync may overwrite cache briefly 583 + - Next sync will include the new annotation 584 + - User sees brief flicker but data is safe 585 + 586 + **Future optimization**: Merge instead of replace 587 + ```typescript 588 + // Instead of replacing entire array 589 + await browser.storage.local.set({ annotations: freshFromPDS }); 590 + 591 + // Merge with local pending writes (more complex) 592 + const { annotations: cached } = await browser.storage.local.get('annotations'); 593 + const merged = mergeCachedAndFresh(cached, freshFromPDS); 594 + await browser.storage.local.set({ annotations: merged }); 595 + ``` 596 + 597 + ### 5. Network Failures 598 + 599 + **Scenario**: PDS unreachable during sync 600 + 601 + **Handling**: 602 + ```typescript 603 + async function syncFromPDS() { 604 + try { 605 + // ... sync logic 606 + } catch (error) { 607 + console.error('[background] Sync failed:', error); 608 + // Keep stale cache 609 + // Set error flag? 610 + await browser.storage.local.set({ 611 + syncError: error.message, 612 + lastSyncAttempt: Date.now() 613 + }); 614 + } 615 + } 616 + ``` 617 + 618 + Show in sidepanel: 619 + ```typescript 620 + const { syncError, lastSync } = await browser.storage.local.get(['syncError', 'lastSync']); 621 + if (syncError) { 622 + showWarning(`Sync failed: ${syncError}. Last successful sync: ${new Date(lastSync)}`); 623 + } 624 + ``` 625 + 626 + ### 6. OAuth Session Expiry 627 + 628 + **Scenario**: Session expires mid-operation 629 + 630 + **Handling**: 631 + ```typescript 632 + try { 633 + await createAnnotation(annotation); 634 + } catch (error) { 635 + if (error.message.includes('401') || error.message.includes('auth')) { 636 + // Clear session 637 + await clearSession(); 638 + // Show login UI 639 + showAuthSection(); 640 + alert('Session expired. Please log in again.'); 641 + } else { 642 + throw error; 643 + } 644 + } 645 + ``` 646 + 647 + --- 648 + 649 + ## Manual Testing Checklist 650 + 651 + ### Basic Functionality 652 + - [ ] Install extension → background syncs on startup 653 + - [ ] Login → session saved, sync triggered 654 + - [ ] Select text → sidepanel shows selection 655 + - [ ] Create annotation → saves to PDS → sync → highlight appears 656 + - [ ] Reload page → highlights persist from cache 657 + - [ ] Logout → cache cleared 658 + 659 + ### Tab Management 660 + - [ ] Open Tab A, create annotation 661 + - [ ] Switch to Tab B (different URL) → sidepanel shows Tab B state 662 + - [ ] Switch back to Tab A → sidepanel shows Tab A state 663 + - [ ] Tab A and Tab B same URL → highlights appear in both 664 + 665 + ### URL Changes 666 + - [ ] Navigate to new page (full reload) → highlights clear → new page's highlights load 667 + - [ ] SPA navigation (pushState) → highlights clear → new highlights load 668 + - [ ] Back button → highlights update correctly 669 + 670 + ### Comments 671 + - [ ] Add comment to annotation → saves to PDS → sync → appears in UI 672 + - [ ] Reply to comment → nested threading works 673 + - [ ] Collapse/expand threads 674 + 675 + ### Edge Cases 676 + - [ ] Open sidepanel on chrome:// page → friendly error message 677 + - [ ] Open sidepanel before content script ready → graceful handling 678 + - [ ] Create annotation while offline → error shown, cache not corrupted 679 + - [ ] Session expires during annotation creation → auth error shown 680 + 681 + ### Cross-Tab Behavior 682 + - [ ] Tab A: select text "foo" 683 + - [ ] Tab B: select text "bar" 684 + - [ ] Sidepanel on Tab A shows "foo" 685 + - [ ] Switch to Tab B → sidepanel shows "bar" 686 + - [ ] No cross-contamination ✓ 687 + 688 + ### Storage Integration 689 + - [ ] Create annotation → `storage.local.annotations` updates 690 + - [ ] Content script sees `storage.onChanged` → re-renders 691 + - [ ] Sidepanel sees `storage.onChanged` → re-renders 692 + - [ ] Multiple tabs with same URL → all update simultaneously 693 + 694 + --- 695 + 696 + ## Performance Considerations 697 + 698 + ### Sync Frequency 699 + - **On startup**: Always sync (fresh data) 700 + - **On user action**: Sync after PDS writes 701 + - **Periodic**: Not needed (cache persists across sessions) 702 + 703 + ### Storage Reads 704 + - Content script: 1 read on page load 705 + - Sidepanel: 1 read on open + 1 per tab switch 706 + - Both watch for changes (no additional reads) 707 + 708 + ### Storage Writes 709 + - Background: 1 write per sync 710 + - Frequency: ~1-5 writes per session (low) 711 + 712 + ### Filtering Performance 713 + ```typescript 714 + // Current: O(n) where n = total annotations 715 + const filtered = annotations.filter(a => a.target[0]?.source === url); 716 + 717 + // With 1000 annotations, 10 for current URL 718 + // Time: ~1ms (negligible) 719 + 720 + // Future optimization: Index by URL in storage 721 + // Time: O(1) lookup 722 + ``` 723 + 724 + **Verdict**: Current approach sufficient, optimize only if users report slowness with >10k annotations. 725 + 726 + --- 727 + 728 + ## Future Enhancements (Out of Scope) 729 + 730 + 1. **Optimistic Updates**: Update cache immediately, sync in background 731 + 2. **Incremental Sync**: Only fetch annotations created after `lastSync` 732 + 3. **URL Indexing**: Store `Map<url, Annotation[]>` for O(1) filtering 733 + 4. **Pagination**: Load annotations on-demand for large collections 734 + 5. **Conflict Resolution**: Merge local changes with remote during sync 735 + 6. **Background Sync API**: Use Service Worker Background Sync for offline support 736 + 7. **Cross-Device Sync**: Real-time updates via WebSocket/AT Protocol firehose 737 + 738 + --- 739 + 740 + ## Migration Path 741 + 742 + ### From Current Architecture 743 + 744 + 1. Keep existing code working 745 + 2. Implement storage-first in parallel 746 + 3. Feature flag to switch between old/new 747 + 4. Test thoroughly 748 + 5. Remove old code 749 + 750 + **No data migration needed**: Both read from same PDS, cache is ephemeral. 751 + 752 + --- 753 + 754 + ## Summary 755 + 756 + This storage-first architecture simplifies the extension by: 757 + - ✅ Eliminating race conditions (no request-response for annotations) 758 + - ✅ Removing cross-tab contamination (each reads from shared cache) 759 + - ✅ Surviving SW suspension (cache persists in storage.local) 760 + - ✅ Automatic updates (storage.onChanged events) 761 + - ✅ Single source of truth (PDS) 762 + 763 + The only message passing is for ephemeral selection state, which is inherently per-tab and request-driven.