boardConverter.js 6.7 KB

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