2
0

boardConverter.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. /**
  2. * Board Conversion Service
  3. * Handles conversion of boards from old database structure to new structure
  4. * without running migrations that could cause downtime
  5. */
  6. import { ReactiveVar } from 'meteor/reactive-var';
  7. import { ReactiveCache } from '/imports/reactiveCache';
  8. // Reactive variables for conversion progress
  9. export const conversionProgress = new ReactiveVar(0);
  10. export const conversionStatus = new ReactiveVar('');
  11. export const conversionEstimatedTime = new ReactiveVar('');
  12. export const isConverting = new ReactiveVar(false);
  13. class BoardConverter {
  14. constructor() {
  15. this.conversionCache = new Map(); // Cache converted board IDs
  16. }
  17. /**
  18. * Check if a board needs conversion
  19. * @param {string} boardId - The board ID to check
  20. * @returns {boolean} - True if board needs conversion
  21. */
  22. needsConversion(boardId) {
  23. if (this.conversionCache.has(boardId)) {
  24. return false; // Already converted
  25. }
  26. try {
  27. const board = ReactiveCache.getBoard(boardId);
  28. if (!board) return false;
  29. // Check if any lists in this board don't have swimlaneId
  30. const lists = ReactiveCache.getLists({
  31. boardId: boardId,
  32. $or: [
  33. { swimlaneId: { $exists: false } },
  34. { swimlaneId: '' },
  35. { swimlaneId: null }
  36. ]
  37. });
  38. return lists.length > 0;
  39. } catch (error) {
  40. console.error('Error checking if board needs conversion:', error);
  41. return false;
  42. }
  43. }
  44. /**
  45. * Convert a board from old structure to new structure
  46. * @param {string} boardId - The board ID to convert
  47. * @returns {Promise<boolean>} - True if conversion was successful
  48. */
  49. async convertBoard(boardId) {
  50. if (this.conversionCache.has(boardId)) {
  51. return true; // Already converted
  52. }
  53. isConverting.set(true);
  54. conversionProgress.set(0);
  55. conversionStatus.set('Starting board conversion...');
  56. try {
  57. const board = ReactiveCache.getBoard(boardId);
  58. if (!board) {
  59. throw new Error('Board not found');
  60. }
  61. // Get the default swimlane for this board
  62. const defaultSwimlane = board.getDefaultSwimline();
  63. if (!defaultSwimlane) {
  64. throw new Error('No default swimlane found for board');
  65. }
  66. // Get all lists that need conversion
  67. const listsToConvert = ReactiveCache.getLists({
  68. boardId: boardId,
  69. $or: [
  70. { swimlaneId: { $exists: false } },
  71. { swimlaneId: '' },
  72. { swimlaneId: null }
  73. ]
  74. });
  75. if (listsToConvert.length === 0) {
  76. this.conversionCache.set(boardId, true);
  77. isConverting.set(false);
  78. return true;
  79. }
  80. conversionStatus.set(`Converting ${listsToConvert.length} lists...`);
  81. const startTime = Date.now();
  82. const totalLists = listsToConvert.length;
  83. let convertedLists = 0;
  84. // Convert lists in batches to avoid blocking the UI
  85. const batchSize = 10;
  86. for (let i = 0; i < listsToConvert.length; i += batchSize) {
  87. const batch = listsToConvert.slice(i, i + batchSize);
  88. // Process batch
  89. await this.processBatch(batch, defaultSwimlane._id);
  90. convertedLists += batch.length;
  91. const progress = Math.round((convertedLists / totalLists) * 100);
  92. conversionProgress.set(progress);
  93. // Calculate estimated time remaining
  94. const elapsed = Date.now() - startTime;
  95. const rate = convertedLists / elapsed; // lists per millisecond
  96. const remaining = totalLists - convertedLists;
  97. const estimatedMs = remaining / rate;
  98. conversionStatus.set(`Converting list ${convertedLists} of ${totalLists}...`);
  99. conversionEstimatedTime.set(this.formatTime(estimatedMs));
  100. // Allow UI to update
  101. await new Promise(resolve => setTimeout(resolve, 10));
  102. }
  103. // Mark as converted
  104. this.conversionCache.set(boardId, true);
  105. conversionStatus.set('Board conversion completed!');
  106. conversionProgress.set(100);
  107. // Clear status after a delay
  108. setTimeout(() => {
  109. isConverting.set(false);
  110. conversionStatus.set('');
  111. conversionProgress.set(0);
  112. conversionEstimatedTime.set('');
  113. }, 2000);
  114. return true;
  115. } catch (error) {
  116. console.error('Error converting board:', error);
  117. conversionStatus.set(`Conversion failed: ${error.message}`);
  118. isConverting.set(false);
  119. return false;
  120. }
  121. }
  122. /**
  123. * Process a batch of lists for conversion
  124. * @param {Array} batch - Array of lists to convert
  125. * @param {string} defaultSwimlaneId - Default swimlane ID
  126. */
  127. async processBatch(batch, defaultSwimlaneId) {
  128. const updates = batch.map(list => ({
  129. _id: list._id,
  130. swimlaneId: defaultSwimlaneId
  131. }));
  132. // Update lists in batch
  133. updates.forEach(update => {
  134. ReactiveCache.getCollection('lists').update(update._id, {
  135. $set: { swimlaneId: update.swimlaneId }
  136. });
  137. });
  138. }
  139. /**
  140. * Format time in milliseconds to human readable format
  141. * @param {number} ms - Time in milliseconds
  142. * @returns {string} - Formatted time string
  143. */
  144. formatTime(ms) {
  145. if (ms < 1000) {
  146. return `${Math.round(ms)}ms`;
  147. }
  148. const seconds = Math.floor(ms / 1000);
  149. const minutes = Math.floor(seconds / 60);
  150. const hours = Math.floor(minutes / 60);
  151. if (hours > 0) {
  152. const remainingMinutes = minutes % 60;
  153. const remainingSeconds = seconds % 60;
  154. return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
  155. } else if (minutes > 0) {
  156. const remainingSeconds = seconds % 60;
  157. return `${minutes}m ${remainingSeconds}s`;
  158. } else {
  159. return `${seconds}s`;
  160. }
  161. }
  162. /**
  163. * Clear conversion cache (useful for testing)
  164. */
  165. clearCache() {
  166. this.conversionCache.clear();
  167. }
  168. }
  169. // Export singleton instance
  170. export const boardConverter = new BoardConverter();