boardConverter.js 6.7 KB

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