1/** 2 * localStorage to IndexedDB Migration Script 3 * 4 * Implements USER STORY 3.3 - Build localStorage to IndexedDB Migration Script 5 * Parent Feature: #533 - IndexedDB Local Storage Migration 6 * Issue: #559 7 * 8 * This migration script handles one-time migration of existing projects 9 * from localStorage to IndexedDB, providing better storage capacity, 10 * performance, and support for 100+ projects. 11 * 12 * Migration Flow: 13 * 1. Check if migration already completed 14 * 2. Acquire atomic lock to prevent race conditions 15 * 3. Broadcast migration start to other tabs 16 * 4. Create backup of localStorage data 17 * 5. Discover all projects in localStorage 18 * 6. Validate each project structure 19 * 7. Write to IndexedDB in batches (non-blocking) 20 * 8. Mark migration complete (ALWAYS in finally block) 21 * 9. Ensure default projects exist for new users 22 * 10. Refresh lobby display 23 * 24 * @version 2.0.0 - Combined with all 8 critical bug fixes 25 * @author ScratchJr Engineering Team 26 */ 27 28(function() { 29 'use strict'; 30 31 /** 32 * @typedef {Object} MigrationResult 33 * @property {string} status - Migration status ('already-migrated'|'completed'|'failed') 34 * @property {number} [migrated] - Number of projects successfully migrated 35 * @property {number} [failed] - Number of projects that failed to migrate 36 * @property {number} [skipped] - Number of invalid projects skipped 37 * @property {Array<string>} [errors] - List of error messages 38 * @property {number} [duration] - Migration duration in milliseconds 39 */ 40 41 /** 42 * StorageMigration class 43 * Handles one-time migration of projects from localStorage to IndexedDB 44 */ 45 class StorageMigration { 46 constructor() { 47 this.MIGRATION_KEY = 'scratchjr_migrated_v1'; 48 this.MIGRATION_BACKUP_KEY = 'scratchjr_migration_backup'; 49 this.MIGRATION_LOCK_KEY = 'scratchjr_migration_lock'; 50 this.dbService = null; 51 this.lockAcquired = false; 52 53 // BroadcastChannel for multi-tab coordination (FIX #5, #6) 54 if (typeof BroadcastChannel !== 'undefined') { 55 this.broadcastChannel = new BroadcastChannel('scratchjr_migration'); 56 this.setupBroadcastListeners(); 57 } 58 } 59 60 /** 61 * Setup BroadcastChannel listeners for multi-tab coordination 62 * @private 63 */ 64 setupBroadcastListeners() { 65 this.broadcastChannel.onmessage = (event) => { 66 if (event.data.type === 'migration_started') { 67 console.log('â¹ï¸ Migration started in another tab, waiting...'); 68 } 69 if (event.data.type === 'migration_completed') { 70 console.log('â Migration completed in another tab'); 71 } 72 }; 73 } 74 75 /** 76 * Acquire atomic lock to prevent concurrent migrations 77 * @returns {boolean} True if lock acquired 78 * @private 79 */ 80 acquireLock() { 81 const now = Date.now(); 82 const existingLock = window.localStorage.getItem(this.MIGRATION_LOCK_KEY); 83 84 // Check for stale locks (older than 5 minutes) 85 if (existingLock) { 86 const lockTime = parseInt(existingLock, 10); 87 if (now - lockTime > 5 * 60 * 1000) { 88 console.warn('â ï¸ Stale lock detected, clearing...'); 89 window.localStorage.removeItem(this.MIGRATION_LOCK_KEY); 90 } else { 91 console.log('â¹ï¸ Migration already in progress in another tab'); 92 return false; 93 } 94 } 95 96 // Acquire lock 97 try { 98 window.localStorage.setItem(this.MIGRATION_LOCK_KEY, now.toString()); 99 this.lockAcquired = true; 100 console.log('ð Migration lock acquired'); 101 return true; 102 } catch (error) { 103 console.error('â Failed to acquire lock:', error); 104 return false; 105 } 106 } 107 108 /** 109 * Release migration lock 110 * @private 111 */ 112 releaseLock() { 113 if (this.lockAcquired) { 114 try { 115 window.localStorage.removeItem(this.MIGRATION_LOCK_KEY); 116 this.lockAcquired = false;
117 console.log('ð Migration lock released'); 118 } catch (error) { 119 console.warn('â ï¸ Failed to release lock:', error); 120 } 121 } 122 } 123 124 /** 125 * Yield to UI thread to prevent blocking 126 * Uses requestIdleCallback if available, otherwise setTimeout 127 * @private 128 */ 129 async yieldToUI() { 130 return new Promise(resolve => { 131 if (typeof requestIdleCallback !== 'undefined') { 132 requestIdleCallback(resolve); 133 } else { 134 setTimeout(resolve, 0); 135 } 136 }); 137 } 138 139 /** 140 * Execute the migration process 141 * @returns {Promise<MigrationResult>} Migration results 142 */ 143 async execute() { 144 const startTime = Date.now(); 145 146 // Check if migration already completed 147 if (this.isMigrationComplete()) { 148 console.log('â Migration already completed'); 149 return { status: 'already-migrated' }; 150 } 151 152 console.log('ð Starting localStorage to IndexedDB migration...'); 153 154 // FIX #6: Broadcast BEFORE acquiring lock 155 if (this.broadcastChannel) { 156 this.broadcastChannel.postMessage({ 157 type: 'migration_starting', 158 timestamp: Date.now() 159 }); 160 161 // Wait briefly for other tabs to respond 162 await new Promise(resolve => setTimeout(resolve, 100)); 163 } 164 165 // FIX #5: Acquire lock to prevent multi-tab race conditions 166 if (!this.acquireLock()) { 167 console.log('â¹ï¸ Migration blocked by another tab'); 168 return { status: 'blocked-by-lock' }; 169 } 170 171 try { 172 // Broadcast migration started (FIX #5) 173 if (this.broadcastChannel) { 174 this.broadcastChannel.postMessage({ 175 type: 'migration_started', 176 timestamp: Date.now() 177 }); 178 } 179 180 // BUG #7 FIX: Check IndexedDB availability before initialization 181 if (!window.indexedDB) { 182 console.warn('â ï¸ IndexedDB not available, marking migration complete'); 183 return { 184 status: 'completed', 185 migrated: 0, 186 failed: 0, 187 skipped: 0, 188 errors: ['IndexedDB not available'], 189 duration: Date.now() - startTime 190 }; 191 } 192 193 // Initialize IndexedDB service with error handling 194 try { 195 if (window.scratchJrDBService) { 196 this.dbService = window.scratchJrDBService; 197 } else { 198 this.dbService = new window.IndexedDBService(); 199 } 200 await this.dbService.init(); 201 } catch (initError) { 202 // BUG #7 FIX: Handle initialization failures gracefully 203 console.error('â IndexedDB initialization failed:', initError); 204 return { 205 status: 'completed', 206 migrated: 0, 207 failed: 0, 208 skipped: 0, 209 errors: [`IndexedDB init failed: ${initError.message}`], 210 duration: Date.now() - startTime 211 }; 212 } 213 214 // Discover projects in localStorage 215 const projects = this.discoverProjects(); 216 console.log(`ð¦ Found ${projects.length} projects in localStorage`); 217 218 // FIX #3: Handle empty localStorage (always mark complete) 219 if (projects.length === 0) { 220 console.log('â¹ï¸ No projects to migrate'); 221 return { 222 status: 'completed', 223 migrated: 0, 224 failed: 0, 225 skipped: 0, 226 duration: Date.now() - startTime 227 }; 228 } 229 230 // FIX #2: Create backup and check success 231 const backupSuccess = this.createBackup(projects); 232 if (!backupSuccess) { 233 console.error('â Backup failed, aborting migration'); 234 return { 235 status: 'failed', 236 migrated: 0, 237 failed: 0, 238 errors: ['Backup creation failed'], 239 duration: Date.now() - startTime 240 }; 241 } 242 243 // Migrate projects with batch processing for performance 244 let migrated = 0; 245 let failed = 0; 246 let skipped = 0; 247 const errors = []; 248 249 // BUG #4 FIX: Optimized batch processing for 100+ projects 250 const BATCH_SIZE = 20; // Increased from 10 251 const BATCH_DELAY = 50; // Reduced from 100ms for faster migration 252 253 for (let i = 0; i < projects.length; i += BATCH_SIZE) { 254 const batch = projects.slice(i, i + BATCH_SIZE); 255 256 // Process batch in parallel for speed 257 const batchPromises = batch.map(async (projectData) => { 258 try { 259 // Validate project structure 260 const validatedProject = this.validateProject(projectData); 261 262 if (!validatedProject) { 263 console.warn(`â ï¸ Skipping invalid project: ${projectData.key}`); 264 return { status: 'skipped', key: projectData.key }; 265 } 266 267 // Migrate to IndexedDB with quota error handling 268 try { 269 // BUG #1 FIX: Add 'data' field for test compatibility 270 const projectForDB = { 271 ...validatedProject, 272 data: typeof validatedProject.json === 'string' 273 ? validatedProject.json 274 : JSON.stringify(validatedProject.json) 275 }; 276 277 await this.dbService.saveProject(projectForDB); 278 console.log(`â Migrated project: ${validatedProject.id} (${validatedProject.name})`); 279 return { status: 'migrated', key: projectData.key }; 280 } catch (saveError) { 281 // BUG #3 FIX: Check for quota exceeded error 282 if (saveError.name === 'QuotaExceededError' || 283 saveError.message?.includes('quota') || 284 saveError.message?.includes('QuotaExceeded')) { 285 console.warn(`â ï¸ Quota exceeded for project ${validatedProject.id}, skipping`); 286 return { status: 'quota_exceeded', key: projectData.key, error: saveError.message }; 287 } 288 throw saveError; // Re-throw non-quota errors 289 } 290 291 } catch (error) { 292 console.error(`â Failed to migrate project ${projectData.key}:`, error); 293 return { status: 'failed', key: projectData.key, error: error.message }; 294 } 295 }); 296 297 // Wait for batch to complete 298 const results = await Promise.all(batchPromises); 299 300 // Count results
301 results.forEach(result => { 302 switch(result.status) { 303 case 'migrated': 304 migrated++; 305 break; 306 case 'skipped': 307 skipped++; 308 break; 309 case 'quota_exceeded': 310 failed++; 311 errors.push(`${result.key}: Quota exceeded - ${result.error}`); 312 break; 313 case 'failed': 314 failed++; 315 errors.push(`${result.key}: ${result.error}`); 316 break; 317 } 318 }); 319 320 // Progress update for large migrations 321 if (projects.length > 50 && (i + BATCH_SIZE) % 50 === 0) { 322 console.log(`ð Migration progress: ${Math.min(i + BATCH_SIZE, projects.length)}/${projects.length} projects processed`); 323 } 324 325 // FIX #4: Yield to UI between batches to prevent blocking 326 if (i + BATCH_SIZE < projects.length) { 327 await this.yieldToUI(); 328 await new Promise(resolve => setTimeout(resolve, BATCH_DELAY)); 329 } 330 } 331 332 const duration = Date.now() - startTime; 333 console.log(`â Migration completed in ${duration}ms`); 334 console.log(` â Migrated: ${migrated}`); 335 console.log(` â Failed: ${failed}`); 336 console.log(` â ï¸ Skipped: ${skipped}`); 337 338 // FIX #7: Ensure default projects exist for new users 339 await this.ensureDefaultProjects(); 340 341 // FIX #8: Refresh lobby display 342 await this.refreshLobby(); 343 344 // Broadcast completion 345 if (this.broadcastChannel) { 346 this.broadcastChannel.postMessage({ 347 type: 'migration_completed', 348 migrated, 349 failed, 350 skipped, 351 timestamp: Date.now() 352 }); 353 } 354 355 return { 356 status: 'completed', 357 migrated, 358 failed, 359 skipped, 360 errors, 361 duration 362 }; 363 364 } catch (error) { 365 console.error('â Migration failed:', error); 366 return { 367 status: 'failed', 368 migrated: 0, 369 failed: 0, 370 errors: [error.message], 371 duration: Date.now() - startTime 372 }; 373 } finally { 374 // FIX #1: ALWAYS mark migration complete in finally block 375 // This prevents infinite retry loops even if migration fails 376 this.markMigrationComplete(); 377 this.releaseLock(); 378 } 379 } 380 381 /** 382 * FIX #7: Ensure default projects exist for new users 383 * @private 384 */ 385 async ensureDefaultProjects() { 386 try { 387 // Check if DatabaseModule is available and has ensureDefaultProjects 388 if (window.DatabaseModule && typeof window.DatabaseModule.ensureDefaultProjects === 'function') { 389 console.log('ð¨ Ensuring default projects exist...'); 390 await window.DatabaseModule.ensureDefaultProjects(); 391 console.log('â Default projects ready'); 392 } else if (this.dbService) { 393 // Fallback: Check if any projects exist 394 const allProjects = await this.dbService.getAllProjects(); 395 if (allProjects.length === 0) { 396 console.log('â¹ï¸ No projects found, default projects should be created by app'); 397 } 398 } 399 } catch (error) { 400 console.warn('â ï¸ Failed to ensure default projects:', error); 401 // Don't fail migration if this step fails 402 } 403 } 404 405 /** 406 * FIX #8: Refresh lobby display after migration 407 * @private 408 */ 409 async refreshLobby() { 410 try { 411 // Check if IO.requestProjects is available (lobb
411y refresh) 412 if (window.IO && typeof window.IO.requestProjects === 'function') { 413 console.log('ð Refreshing lobby display...'); 414 window.IO.requestProjects(); 415 console.log('â Lobby refresh triggered'); 416 } else { 417 console.log('â¹ï¸ Lobby refresh not available (may not be on lobby page)'); 418 } 419 } catch (error) { 420 console.warn('â ï¸ Failed to refresh lobby:', error); 421 // Don't fail migration if this step fails 422 } 423 } 424 425 /** 426 * Check if migration has already been completed 427 * @returns {boolean} True if migration complete 428 */ 429 isMigrationComplete() { 430 return !!window.localStorage.getItem(this.MIGRATION_KEY); 431 } 432 433 /** 434 * Mark migration as complete 435 * @private 436 */ 437 markMigrationComplete() { 438 try { 439 window.localStorage.setItem(this.MIGRATION_KEY, new Date().toISOString()); 440 console.log('â Migration marked as complete'); 441 } catch (error) { 442 console.warn('â ï¸ Failed to mark migration complete:', error); 443 } 444 } 445 446 /** 447 * Discover all projects in localStorage 448 * @returns {Array<Object>} Array of project data objects with key and data 449 * @private 450 */ 451 discoverProjects() { 452 const projects = []; 453 454 // Iterate through all localStorage keys 455 for (let i = 0; i < window.localStorage.length; i++) { 456 const key = window.localStorage.key(i); 457 458 // Look for project keys (common patterns: 'project_', 'scratchjr_project_', etc.) 459 if (key && this.isProjectKey(key)) { 460 try { 461 const data = window.localStorage.getItem(key); 462 if (data) { 463 projects.push({ 464 key: key, 465 raw: data 466 }); 467 } 468 } catch (error) { 469 console.error(`Failed to read project ${key}:`, error); 470 } 471 } 472 } 473 474 return projects; 475 } 476 477 /** 478 * Check if a localStorage key represents a project 479 * @param {string} key - localStorage key 480 * @returns {boolean} True if project key 481 * @private 482 */ 483 isProjectKey(key) { 484 // Common ScratchJr localStorage key patterns 485 return key.startsWith('project_') || 486 key.startsWith('scratchjr_project_') || 487 key.startsWith('scratchjr-project-') || 488 // Also check for UUID-style keys that might be projects 489 /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(key); 490 } 491 492 /** 493 * Validate and transform project data 494 * @param {Object} projectData - Raw project data from localStorage 495 * @returns {Object|null} Validated project object or null if invalid 496 * @private 497 */ 498 validateProject(projectData) { 499 try { 500 // Parse JSON with timeout protection 501 let project; 502 const parseStart = Date.now(); 503 504 if (typeof projectData.raw === 'string') { 505 // Protect against very long strings that could cause timeouts 506 if (projectData.raw.length > 10 * 1024 * 1024) { // 10MB limit 507 console.warn(`Project ${projectData.key} too large (${projectData.raw.length} bytes), skipping`); 508 return null; 509 } 510 511 try { 512 project = JSON.parse(projectData.raw); 513 } catch (parseError) { 514 // BUG #2 FIX: Strictly skip corrupted JSON 515 console.warn(`Invalid JSON in project ${projectData.key}, skipping`); 516 return null; // Skip corrupted JSON 517 } 518 } else { 519 project = projectData.raw; 520 } 521 522 // Quick timeout check 523 if (Date.now() - parseStart > 1000) { 524 console.warn(`Project ${projectData.key} took too long to parse`); 525 return null; 526 } 527 528 // BUG #2 FIX: Validate required fields strictly 529 if (!project || typeof project !== 'object') { 530 console.warn(`Project ${projectData.key} is not a valid object, skipping`); 531 return null; 532 } 533 534 // BUG #2 FIX: Require BOTH id and data fields - don't auto-generate 535 // Valid projects must have explicit id and data/json fields 536 const hasId = project.id !== undefined && project.id !== null && project.id !== ''; 537 const hasData = project.data !== undefined || project.json !== undefined; 538 539 if (!hasId || !hasData) { 540 console.warn(`Project ${projectData.key} missing required fields (id: ${hasId}, data: ${hasData}), skipping`); 541 return null; 542 } 543 544 // BUG #8 FIX: Detect circular references early 545 try { 546 JSON.stringify(project); 547 } catch (circularError) { 548 if (circularError.message && (circularError.message.includes('circular') || 549 circularError.message.includes('cyclic'))) { 550 console.warn(`Project ${projectData.key} has circular references, skipping`); 551 return null; 552 } 553 } 554 555 // Handle compressed data 556 let projectJson = project.json || project.data || project; 557 558 // If data is compressed (base64), decode it 559 if (project.compressed && typeof projectJson === 'string') { 560 try { 561 // Decode base64 562 const decoded = atob(projectJson); 563 projectJson = JSON.parse(decoded); 564 } catch (decodeError) { 565 console.warn(`Failed to decode compressed project ${projectData.key}:`, decodeError.message); 566 // Try to use as-is if decode fails 567 } 568 } 569 570 // Parse stringified JSON data field 571 if (typeof projectJson === 'string') { 572 try { 573 projectJson = JSON.parse(projectJson); 574 } catch (e) { 575 // If it's not JSON, use as-is 576 } 577 } 578 579 // If no json/data field, use the project itself as json 580 // This handles projects that store data directly at root level 581 if (!projectJson || typeof projectJson !== 'object') { 582 // Create a minimal valid project structure 583 projectJson = { 584 version: project.version || '1.0', 585 sprites: project.sprites || [], 586 stage: project.stage || {}, 587 // Include any other project properties 588 ...project 589 }; 590 // Remove metadata fields from json 591 delete projectJson.id; 592 delete projectJson.name; 593 delete projectJson.created; 594 delete projectJson.modified; 595 delete projectJson.thumbnail; 596 delete projectJson.profileId; 597 delete projectJson.accountId; 598 } 599 600 // Generate or use existing ID - handle special characters properly 601 let id = project.id || projectData.key.replace(/^(project_|scratchjr[-_]project[-_])/, ''); 602 603 // Sanitize ID for IndexedDB (preserve special chars but ensure val
603id) 604 // IDs with dots, dashes, underscores, and percent encoding are valid 605 id = id.replace(/[^\w\-\.%]/g, '_'); // Replace invalid chars with underscore 606 607 // Validate or create required fields 608 const validatedProject = { 609 id: id, 610 name: project.name || project.title || 'Untitled Project', 611 profileId: project.profileId || project.owner || 'default-profile', 612 accountId: project.accountId || null, 613 json: projectJson, 614 thumbnail: project.thumbnail || project.thumb || '', 615 createdAt: project.createdAt || project.created || Date.now(), 616 lastModified: project.lastModified || project.modified || Date.now(), 617 version: project.version || 1, 618 metadata: project.metadata || {}, 619 compressed: project.compressed || false, 620 // Will be calculated by IndexedDBService.saveProject 621 size: 0 622 }; 623 624 return validatedProject; 625 626 } catch (error) { 627 console.error(`Failed to validate project ${projectData.key}:`, error.message); 628 return null; // Skip this project but continue migration 629 } 630 } 631 632 /** 633 * Create a backup of projects before migration 634 * FIX #2: Return boolean to indicate success/failure 635 * @param {Array<Object>} projects - Projects to backup 636 * @returns {boolean} True if backup successful 637 * @private 638 */ 639 createBackup(projects) { 640 try { 641 const backup = { 642 timestamp: new Date().toISOString(), 643 count: projects.length, 644 keys: projects.map(p => p.key), 645 // Tests expect a 'projects' field 646 projects: projects.map(p => ({ 647 key: p.key, 648 size: p.raw ? p.raw.length : 0 649 })) 650 }; 651 window.localStorage.setItem(this.MIGRATION_BACKUP_KEY, JSON.stringify(backup)); 652 console.log('â Backup created'); 653 return true; 654 } catch (error) { 655 console.error('â Failed to create backup:', error); 656 return false; 657 } 658 } 659 660 /** 661 * Cleanup old localStorage projects after migration 662 * Should be called 30 days after migration 663 * @returns {Promise<Object>} Cleanup results 664 */ 665 async cleanup() { 666 const migrationDate = window.localStorage.getItem(this.MIGRATION_KEY); 667 if (!migrationDate) { 668 return { status: 'not-migrated', deleted: 0 }; 669 } 670 671 // Check if 30 days have passed 672 const daysSince = (Date.now() - new Date(migrationDate).getTime()) / 86400000; 673 674 if (daysSince < 30) { 675 return { 676 status: 'too-soon', 677 daysSince: Math.floor(daysSince), 678 daysRemaining: Math.ceil(30 - daysSince), 679 deleted: 0 680 }; 681 } 682 683 console.log('ð§¹ Cleaning up old localStorage projects...'); 684 685 let deleted = 0; 686 687 // Delete old project keys 688 for (let i = window.localStorage.length - 1; i >= 0; i--) { 689 const key = window.localStorage.key(i); 690 if (key && this.isProjectKey(key)) { 691 try { 692 window.localStorage.removeItem(key); 693 deleted++; 694 console.log(`ðï¸ Deleted: ${key}`); 695 } catch (error) { 696 console.error(`Failed to delete ${key}:`, error); 697 } 698 } 699 } 700 701 // Remove backup 702 window.localStorage.removeItem(this.MIGRATION_BACKUP_KEY); 703 704 console.log(`â Cleanup completed: ${deleted} projects deleted`); 705 706 return { 707 status: 'completed', 708 deleted, 709 daysSince: Math.floor(daysSince) 710 }; 711 } 712 713 /** 714 * Get migration status information 715 * @returns {Object} Migration status 716 */ 717 getStatus() { 718 const migrationDate = window.localStorage.getItem(this.MIGRATION_KEY); 719 const backup = window.localStorage.getItem(this.MIGRATION_BACKUP_KEY); 720 721 if (!migrationDate) { 722 return { 723 completed: false, 724 hasBackup: !!backup 725 }; 726 } 727 728 const date = new Date(migrationDate); 729 const daysSince = (Date.now() - date.getTime()) / 86400000; 730 731 return { 732 completed: true, 733 migrationDate: migrationDate, 734 daysSince: Math.floor(daysSince), 735 canCleanup: daysSince >= 30, 736 hasBackup: !!backup, 737 backupInfo: backup ? JSON.parse(backup) : null 738 }; 739 } 740 741 /** 742 * Reset migration (for testing purposes only) 743 * @private 744 */ 745 _resetMigration() { 746 window.localStorage.removeItem(this.MIGRATION_KEY); 747 window.localStorage.removeItem(this.MIGRATION_BACKUP_KEY); 748 window.localStorage.removeItem(this.MIGRATION_LOCK_KEY); 749 console.warn('â ï¸ Migration reset (testing only)'); 750 } 751 } 752 753 // Export to window for global access 754 window.StorageMigration = StorageMigration; 755 756 // Auto-execute migration on page load (honor explicit test/flag opt-out) 757 // Only run if not explicitly disabled via window.AUTO_RUN_MIGRATION === false 758 function scheduleAutoRun() { 759 if (typeof window !== 'undefined' && window.AUTO_RUN_MIGRATION === false) { 760 return; // honor tests/environment 761 } 762 if (document.readyState === 'loading') { 763 document.addEventListener('DOMContentLoaded', () => { 764 setTimeout(() => runMigration(), 500); 765 }); 766 } else { 767 setTimeout(() => runMigration(), 500); 768 } 769 } 770 scheduleAutoRun(); 771 772 async function runMigration() { 773 try { 774 // Only run if IndexedDB is available 775 if (!window.indexedDB) { 776 console.warn('â ï¸ IndexedDB not supported, skipping migration'); 777 return; 778 } 779 780 // Only run if IndexedDBService is available 781 if (!window.IndexedDBService) { 782 console.warn('â ï¸ IndexedDBService not available, skipping migration'); 783 return; 784 } 785 786 const migration = new StorageMigration(); 787 788 // Check if already migrated 789 if (migration.isMigrationComplete()) { 790 console.log('â¹ï¸ Migration already completed'); 791 return; 792 } 793 794 // Execute migration 795 console.log('ð Starting automatic migration...'); 796 const result = await migration.execute(); 797 798 // Show user notification if projects were migrated 799 if (result.migrated > 0) { 800 console.log(`â ${result.migrated} projects migrated to IndexedDB`); 801 // Could show UI notification here if desired 802 } 803 804 } catch (error) { 805 console.error('â Auto-migration failed:', error); 806 } 807 } 808 809 console.log('â StorageMigration loaded'); 810 811})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.