import { Injectable, Logger } from '@nestjs/common'; import { BrandHiboutikService } from './brand.hiboutik.service'; import { CategoryHiboutikService } from './category.hiboutik.service'; import { CustomerHiboutikService } from './customer.hiboutik.service'; import { SupplierHiboutikService } from './supplier.hiboutik.service'; import { ProductHiboutikService } from './product.hiboutik.service'; import { ReleaseLock, SyncLockDatabaseRepository } from '../../database/syncLock.database.repository'; /** Entry point of the Hiboutik → database synchronization: runs each entity sync in dependency order. */ @Injectable() export class HiboutikService { private readonly logger = new Logger('HiboutikService'); /** Cheap in-process guard, checked before the database lock (which is the one that holds across instances). */ private syncInProgress = false; constructor( private readonly brandHiboutikService: BrandHiboutikService, private readonly categoryHiboutikService: CategoryHiboutikService, private readonly customerHiboutikService: CustomerHiboutikService, private readonly productHiboutikService: ProductHiboutikService, private readonly supplierHiboutikService: SupplierHiboutikService, private readonly syncLock: SyncLockDatabaseRepository, ) {} get isSyncInProgress(): boolean { return this.syncInProgress; } /** * Starts a full synchronization in the background, if no other one is running on this instance or any other * (Postgres advisory lock, held until the synchronization ends). * @returns false if a synchronization is already running (nothing is started). * @throws if the lock cannot be requested (e.g. database unreachable). */ async startSync(): Promise { if (this.syncInProgress) return false; // Set before the first await so that concurrent calls on this instance are refused while the lock is requested this.syncInProgress = true; let release: ReleaseLock | null; try { release = await this.syncLock.tryAcquire(); } catch (error) { this.syncInProgress = false; throw error; } if (!release) { this.syncInProgress = false; return false; } void this.runLocked(release); return true; } /** Runs the synchronization, then always releases the lock. */ private async runLocked(release: ReleaseLock): Promise { try { await this.syncAll(); } catch (error) { this.logger.error('Global Hiboutik synchronization crashed', (error as Error)?.stack); } finally { await release(); this.syncInProgress = false; } } /** Runs every sync step in order. A failing step is logged and does not stop the next ones. */ async syncAll() { this.logger.log('Starting global Hiboutik synchronization'); // Order matters: products connect to brands, categories and suppliers by hiboutikId. const steps: [string, () => Promise][] = [ ['brands', () => this.brandHiboutikService.syncBrands()], ['categories', () => this.categoryHiboutikService.syncCategories()], ['suppliers', () => this.supplierHiboutikService.syncSuppliers()], ['products', () => this.productHiboutikService.syncProducts()], ['customers', () => this.customerHiboutikService.syncCustomers()], ]; const failedSteps: string[] = []; for (const [name, step] of steps) { try { await step(); } catch (error) { failedSteps.push(name); this.logger.error(`Hiboutik ${name} sync failed`, (error as Error)?.stack); } } if (failedSteps.length > 0) { this.logger.warn(`Global Hiboutik synchronization ended with errors (failed steps: ${failedSteps.join(', ')})`); } else { this.logger.log('Global Hiboutik synchronization ended successfully'); } } }