Skip to main content

Programmation Asynchrone Node.js dans Softyflow

Ce guide explique comment utiliser efficacement les modèles de programmation asynchrone Node.js dans les scripts Softyflow, en mettant l'accent sur la syntaxe async/await et les meilleures pratiques pour écrire des scripts robustes et performants.

Comprendre la Programmation Asynchrone dans SoftyflowLien direct vers Comprendre la Programmation Asynchrone dans Softyflow

L'environnement d'exécution JavaScript de Softyflow est construit sur Node.js, ce qui signifie que toutes les opérations d'E/S (requêtes de base de données, appels API, opérations sur fichiers) sont asynchrones. Comprendre comment gérer correctement les opérations asynchrones est crucial pour écrire des scripts efficaces.

L'Architecture Basée sur les PromessesLien direct vers L'Architecture Basée sur les Promesses

Toutes les méthodes SDK de Softyflow retournent des Promesses, qui représentent l'achèvement éventuel (ou l'échec) des opérations asynchrones :

// ❌ Incorrect - Cela ne fonctionnera pas comme prévu
let users = SF.collection.find("users", {status: "active"}); // Cela retourne un objet Promesse, pas les données

// ✅ Correct - Utiliser await pour obtenir les données réelles
let users = await SF.collection.find("users", {status: "active"});

Fondamentaux Async/AwaitLien direct vers Fondamentaux Async/Await

Modèle Async/Await de BaseLien direct vers Modèle Async/Await de Base

Le mot-clé await ne peut être utilisé que dans les fonctions marquées avec async. Dans les scripts Softyflow, le contexte d'exécution est automatiquement async, vous pouvez donc utiliser await directement :

// Softyflow script - automatically in async context
let user = await SF.user.getUserById("6042551d231e8c8ade19bc09");
let orders = await SF.collection.find("orders", {userId: user._id});

return {
  user: user,
  orderCount: orders.length,
  totalAmount: orders.reduce((sum, order) => sum + order.amount, 0)
};

Exécution Séquentielle vs Exécution ParallèleLien direct vers Exécution Séquentielle vs Exécution Parallèle

Comprendre quand les opérations s'exécutent séquentiellement par rapport en parallèle est crucial pour les performances :

Exécution Séquentielle (Les Opérations Dépendent les Unes des Autres)Lien direct vers Exécution Séquentielle (Les Opérations Dépendent les Unes des Autres)

// ✅ Bon - Quand chaque opération dépend de la précédente
let user = await SF.user.getUserById(userId);
let userGroup = await SF.user.getGroupById(user.groupId);
let permissions = await SF.collection.findOne("permissions", {groupId: userGroup._id});

return {
  user: user,
  group: userGroup,
  permissions: permissions
};

Exécution Parallèle (Opérations Indépendantes)Lien direct vers Exécution Parallèle (Opérations Indépendantes)

// ✅ Mieux - Quand les opérations sont indépendantes, les exécuter en parallèle
let [user, recentOrders, supportTickets] = await Promise.all([
  SF.user.getUserById(userId),
  SF.collection.find("orders", {userId: userId, createdAt: {$gte: lastMonth}}),
  SF.collection.find("tickets", {userId: userId, status: "open"})
]);

return {
  user: user,
  recentOrdersCount: recentOrders.length,
  openTicketsCount: supportTickets.length
};

Modèles Asynchrones Courants de SoftyflowLien direct vers Modèles Asynchrones Courants de Softyflow

1. Opérations de Base de DonnéesLien direct vers 1. Opérations de Base de Données

Opérations sur un Seul DocumentLien direct vers Opérations sur un Seul Document

// Find and update pattern
let existingRecord = await SF.collection.findOne("customers", {email: customerEmail});

if (existingRecord) {
  // Update existing customer
  await SF.collection.updateOne("customers", 
    {email: customerEmail}, 
    {lastLogin: new Date(), loginCount: existingRecord.loginCount + 1}
  );
} else {
  // Create new customer
  await SF.collection.insertOne("customers", {
    email: customerEmail,
    createdAt: new Date(),
    loginCount: 1
  });
}

Opérations en MasseLien direct vers Opérations en Masse

// Traitement efficace en masse
let usersToUpdate = await SF.collection.find("users", {status: "pending"});

// Préparer les opérations en masse
let bulkOps = usersToUpdate.map(user => ({
  updateOne: {
    filter: {_id: user._id},
    update: {$set: {status: "active", activatedAt: new Date()}}
  }
}));

// Exécuter toutes les mises à jour en une seule opération
if (bulkOps.length > 0) {
  await SF.collection.bulkWrite("users", bulkOps);
}

return {processed: usersToUpdate.length};

2. Modèles d'Intégration d'APILien direct vers 2. Modèles d'Intégration d'API

// External API call with error handling
try {
  let response = await SF.utils.axios.get(`https://api.external-service.com/users/${userId}`, {
    headers: {
      'Authorization': `Bearer ${apiToken}`,
      'Content-Type': 'application/json'
    },
    timeout: 5000 // 5 second timeout
  });
  
  // Process the response
  let externalUser = response.data;
  
  // Update local database with external data
  await SF.collection.updateOne("users", 
    {_id: userId}, 
    {
      externalId: externalUser.id,
      lastSyncAt: new Date(),
      syncStatus: "success"
    }
  );
  
  return {success: true, externalId: externalUser.id};
  
} catch (error) {
  // Handle API errors gracefully
  console.error('External API call failed:', error.message);
  
  // Update sync status
  await SF.collection.updateOne("users", 
    {_id: userId}, 
    {syncStatus: "failed", lastSyncError: error.message}
  );
  
  return {success: false, error: error.message};
}

3. File Processing PatternsLien direct vers 3. File Processing Patterns

// Générer un PDF et l'enregistrer dans la collection
try {
  // Obtenir le modèle et les données en parallèle
  let [template, customerData] = await Promise.all([
    SF.file.getFileContentById(templateId),
    SF.collection.findOne("customers", {_id: customerId})
  ]);

  // Générer le PDF
  let pdfFile = await SF.file.generatePDFFromTemplate(null, templateId, {
    variables: {
      customer: customerData,
      generatedAt: new Date().toLocaleDateString(),
      invoiceNumber: await SF.utils.nextValue("invoice", 6)
    },
    SF_pdfName: `invoice-${customerData.code}.pdf`
  });

  // Enregistrer la référence dans la collection orders
  await SF.collection.updateOne("orders",
    {customerId: customerId},
    {pdfFileId: pdfFile._id, pdfGeneratedAt: new Date()}
  );

  return {pdfGenerated: true, fileId: pdfFile._id};

} catch (error) {
  console.error('Génération du PDF échouée :', error);
  return {pdfGenerated: false, error: error.message};
}

Meilleures Pratiques pour les Scripts SoftyflowLien direct vers Meilleures Pratiques pour les Scripts Softyflow

1. Gestion des ErreursLien direct vers 1. Gestion des Erreurs

Enveloppez toujours les opérations susceptibles d'échouer dans les blocs try-catch :

// ✅ Good - Robust error handling
async function processOrder(orderId) {
  try {
    let order = await SF.collection.findOne("orders", {_id: orderId});
    
    if (!order) {
      throw new Error(`Order ${orderId} not found`);
    }
    
    // Process order...
    let result = await processPayment(order);
    
    return {success: true, result: result};
    
  } catch (error) {
    console.error('Order processing failed:', error.message);
    
    // Log error for debugging
    await SF.collection.insertOne("error_logs", {
      operation: "processOrder",
      orderId: orderId,
      error: error.message,
      timestamp: new Date()
    });
    
    return {success: false, error: error.message};
  }
}

2. Optimisation des PerformancesLien direct vers 2. Optimisation des Performances

Utiliser les Opérations Parallèles Quand C'est PossibleLien direct vers Utiliser les Opérations Parallèles Quand C'est Possible

// ❌ Lent - Opérations séquentielles
let user = await SF.user.getUserById(userId);
let orders = await SF.collection.find("orders", {userId: userId});
let preferences = await SF.collection.findOne("preferences", {userId: userId});

// ✅ Rapide - Opérations parallèles
let [user, orders, preferences] = await Promise.all([
  SF.user.getUserById(userId),
  SF.collection.find("orders", {userId: userId}),
  SF.collection.findOne("preferences", {userId: userId})
]);

Minimiser les Requêtes de Base de DonnéesLien direct vers Minimiser les Requêtes de Base de Données

// ❌ Inefficace - Plusieurs requêtes
let results = [];
for (let userId of userIds) {
  let user = await SF.user.getUserById(userId);
  results.push(user);
}

// ✅ Efficace - Requête d'agrégation unique
let results = await SF.collection.aggregate("users", [
  {$match: {_id: {$in: userIds}}},
  {$project: {firstName: 1, lastName: 1, email: 1}}
]);

3. Gestion des VariablesLien direct vers 3. Gestion des Variables

// ✅ Good - Clear variable organization
let processData = {
  // Input data validation
  customerId: customerId || null,
  orderDate: orderDate || new Date(),
  items: Array.isArray(items) ? items : [],
  
  // Calculated values
  subtotal: 0,
  tax: 0,
  total: 0,
  
  // Status tracking
  processed: false,
  errors: []
};

// Validate required data
if (!processData.customerId) {
  processData.errors.push("Customer ID is required");
}

if (processData.items.length === 0) {
  processData.errors.push("Order must contain at least one item");
}

if (processData.errors.length > 0) {
  return processData; // Return early with errors
}

// Process the order...

4. Traitement ConditionnelLien direct vers 4. Traitement Conditionnel

// ✅ Bon - Logique conditionnelle propre avec opérations asynchrones
let approvalRequired = false;
let approvers = [];

// Vérifier si une approbation est nécessaire en fonction du montant
if (orderTotal > 10000) {
  approvalRequired = true;
  approvers = await SF.user.getUsersInGroupList(["managers", "directors"]);
} else if (orderTotal > 5000) {
  approvalRequired = true;
  approvers = await SF.user.getUsersinGroup("managers");
}

if (approvalRequired) {
  // Créer une tâche d'approbation
  let approvalTask = await SF.instance.validateTask(currentTaskId, {
    requiresApproval: true,
    approvers: approvers.map(user => user.email),
    amount: orderTotal
  });

  return {
    status: "pending_approval",
    approvers: approvers.length,
    taskId: approvalTask.id
  };
} else {
  // Approbation automatique des petites commandes
  return {
    status: "approved",
    autoApproved: true
  };
}

Modèles AvancésLien direct vers Modèles Avancés

1. Logique de Réessai pour les Services ExternesLien direct vers 1. Logique de Réessai pour les Services Externes

async function callExternalServiceWithRetry(url, data, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      let response = await SF.utils.axios.post(url, data, {
        timeout: 10000
      });
      
      return response.data; // Success, return data
      
    } catch (error) {
      console.log(`Attempt ${attempt} failed:`, error.message);
      
      if (attempt === maxRetries) {
        throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
      }
      
      // Wait before retry (exponential backoff)
      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
    }
  }
}

// Usage
try {
  let result = await callExternalServiceWithRetry("https://api.example.com/process", orderData);
  return {success: true, externalId: result.id};
} catch (error) {
  return {success: false, error: error.message};
}

2. Traitement par Lots avec Suivi de la ProgressionLien direct vers 2. Traitement par Lots avec Suivi de la Progression

async function processBatchWithProgress(items, batchSize = 10) {
  let results = [];
  let processed = 0;
  let errors = [];

  // Traiter les éléments par lots
  for (let i = 0; i < items.length; i += batchSize) {
    let batch = items.slice(i, i + batchSize);

    // Traiter le lot en parallèle
    let batchPromises = batch.map(async (item) => {
      try {
        let result = await SF.collection.insertOne("processed_items", {
          originalId: item.id,
          processedAt: new Date(),
          data: item
        });
        return {success: true, id: result.insertedId};
      } catch (error) {
        return {success: false, error: error.message, item: item};
      }
    });

    let batchResults = await Promise.all(batchPromises);

    // Collecter les résultats et suivre la progression
    for (let result of batchResults) {
      if (result.success) {
        results.push(result);
      } else {
        errors.push(result);
      }
      processed++;
    }

    // Journaliser la progression
    console.log(`${processed}/${items.length} éléments traités`);

    // Optionnel : Mettre à jour la progression dans la base de données
    await SF.collection.updateOne("batch_jobs",
      {_id: jobId},
      {
        processed: processed,
        total: items.length,
        progress: Math.round((processed / items.length) * 100)
      }
    );
  }

  return {
    total: items.length,
    successful: results.length,
    failed: errors.length,
    errors: errors
  };
}

3. Orchestration Complexe de Flux de TravailLien direct vers 3. Orchestration Complexe de Flux de Travail

// Complex workflow with multiple async operations
async function processCompleteOrder(orderData) {
  let workflow = {
    orderId: null,
    customerId: orderData.customerId,
    status: "started",
    steps: [],
    errors: []
  };
  
  try {
    // Step 1: Validate customer
    workflow.steps.push({step: "validate_customer", status: "running"});
    let customer = await SF.user.getUserById(orderData.customerId);
    if (!customer) {
      throw new Error("Customer not found");
    }
    workflow.steps[0].status = "completed";
    
    // Step 2: Check inventory (parallel for all items)
    workflow.steps.push({step: "check_inventory", status: "running"});
    let inventoryChecks = await Promise.all(
      orderData.items.map(item => 
        SF.collection.findOne("inventory", {productId: item.productId})
      )
    );
    
    let insufficientStock = inventoryChecks.filter((stock, index) => 
      !stock || stock.quantity < orderData.items[index].quantity
    );
    
    if (insufficientStock.length > 0) {
      throw new Error("Insufficient inventory for some items");
    }
    workflow.steps[1].status = "completed";
    
    // Step 3: Create order record
    workflow.steps.push({step: "create_order", status: "running"});
    let order = await SF.collection.insertOne("orders", {
      customerId: customer._id,
      items: orderData.items,
      status: "processing",
      createdAt: new Date(),
      total: orderData.items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
    });
    workflow.orderId = order.insertedId;
    workflow.steps[2].status = "completed";
    
    // Step 4: Update inventory (sequential to avoid conflicts)
    workflow.steps.push({step: "update_inventory", status: "running"});
    for (let item of orderData.items) {
      await SF.collection.updateOne("inventory",
        {productId: item.productId},
        {$inc: {quantity: -item.quantity, reserved: item.quantity}}
      );
    }
    workflow.steps[3].status = "completed";
    
    // Step 5: Generate invoice
    workflow.steps.push({step: "generate_invoice", status: "running"});
    let invoiceNumber = await SF.utils.nextValue("invoice", 6);
    let invoice = await SF.file.generatePDFFromTemplate(null, invoiceTemplateId, {
      variables: {
        customer: customer,
        order: order,
        invoiceNumber: invoiceNumber,
        items: orderData.items
      },
      SF_pdfName: `invoice-${invoiceNumber}.pdf`
    });
    workflow.steps[4].status = "completed";
    
    // Step 6: Send confirmation email
    workflow.steps.push({step: "send_confirmation", status: "running"});
    await SF.utils.axios.post("https://api.email-service.com/send", {
      to: customer.email,
      template: "order_confirmation",
      data: {
        customerName: customer.firstName,
        orderId: workflow.orderId,
        invoiceUrl: invoice.downloadUrl
      }
    });
    workflow.steps[5].status = "completed";
    
    workflow.status = "completed";
    
  } catch (error) {
    workflow.status = "failed";
    workflow.errors.push({
      message: error.message,
      step: workflow.steps.findIndex(s => s.status === "running"),
      timestamp: new Date()
    });
    
    // Rollback if necessary
    if (workflow.orderId) {
      await SF.collection.updateOne("orders", 
        {_id: workflow.orderId}, 
        {status: "failed", error: error.message}
      );
    }
  }
  
  // Log workflow completion
  await SF.collection.insertOne("workflow_logs", workflow);
  
  return workflow;
}

Pièges Courants et Comment les ÉviterLien direct vers Pièges Courants et Comment les Éviter

1. Oublier d'Utiliser AwaitLien direct vers 1. Oublier d'Utiliser Await

// ❌ Incorrect - Await manquant
let user = SF.user.getUserById(userId); // C'est une Promesse, pas les données utilisateur
console.log(user.firstName); // Erreur : Impossible de lire la propriété 'firstName' de undefined

// ✅ Correct
let user = await SF.user.getUserById(userId);
console.log(user.firstName); // Fonctionne correctement

2. Utiliser des Opérations Asynchrones dans les Méthodes de Tableau de Manière IncorrecteLien direct vers 2. Utiliser des Opérations Asynchrones dans les Méthodes de Tableau de Manière Incorrecte

// ❌ Incorrect - map() n'attend pas les opérations asynchrones
let users = userIds.map(async (id) => {
  return await SF.user.getUserById(id);
}); // Cela retourne un tableau de Promesses, pas des utilisateurs

// ✅ Correct - Utiliser Promise.all()
let users = await Promise.all(
  userIds.map(id => SF.user.getUserById(id))
);

3. Ne Pas Gérer les Erreurs CorrectementLien direct vers 3. Ne Pas Gérer les Erreurs Correctement

// ❌ Risqué - Les erreurs non gérées peuvent bloquer le script
let result = await SF.collection.insertOne("orders", orderData);

// ✅ Sûr - Toujours gérer les erreurs potentielles
try {
  let result = await SF.collection.insertOne("orders", orderData);
  return {success: true, orderId: result.insertedId};
} catch (error) {
  console.error('Création de la commande échouée :', error);
  return {success: false, error: error.message};
}

4. Traitement Séquentiel InefficaceLien direct vers 4. Traitement Séquentiel Inefficace

// ❌ Lent - Traitement des éléments un par un
for (let item of items) {
  await processItem(item); // Chaque élément attend le précédent
}

// ✅ Rapide - Traiter les éléments en parallèle (si possible)
await Promise.all(items.map(item => processItem(item)));

// ✅ Équilibré - Traiter par lots pour éviter de surcharger le système
for (let i = 0; i < items.length; i += 10) {
  let batch = items.slice(i, i + 10);
  await Promise.all(batch.map(item => processItem(item)));
}

Conseils de Test et de DébogageLien direct vers Conseils de Test et de Débogage

1. Utiliser la Journalisation de la Console de Manière EfficaceLien direct vers 1. Utiliser la Journalisation de la Console de Manière Efficace

console.log('Démarrage du traitement de la commande pour :', orderId);

let order = await SF.collection.findOne("orders", {_id: orderId});
console.log('Commande récupérée :', JSON.stringify(order, null, 2));

if (!order) {
  console.error('Commande non trouvée :', orderId);
  return {error: "Commande non trouvée"};
}

console.log('Traitement de la commande terminé avec succès');

2. Retourner des Informations Détaillées pour le DébogageLien direct vers 2. Retourner des Informations Détaillées pour le Débogage

return {
  success: true,
  orderId: order._id,
  customerEmail: customer.email,
  itemsProcessed: items.length,
  totalAmount: total,
  processingTime: Date.now() - startTime,
  // Inclure les infos de débogage en développement
  debug: {
    inventoryChecks: inventoryResults,
    emailSent: emailResult.success,
    invoiceGenerated: !!invoiceFile
  }
};

3. Utiliser l'Interface de Test de SoftyflowLien direct vers 3. Utiliser l'Interface de Test de Softyflow

  • Ajouter des variables de test avec des données d'échantillon réalistes
  • Tester chaque composant du script séparément
  • Vérifier les types et valeurs des variables
  • Vérifier les chemins de gestion des erreurs

Considérations de PerformanceLien direct vers Considérations de Performance

  1. Minimiser les Requêtes de Base de Données : Utiliser l'agrégation et les opérations en masse si possible
  2. Utiliser le Traitement Parallèle : Exécuter les opérations indépendantes simultanément
  3. Implémenter des Délais d'Expiration : Définir des délais d'expiration raisonnables pour les appels API externes
  4. Mettre en Cache les Données Fréquemment Utilisées : Stocker les données couramment consultées dans les variables
  5. Opérations par Lots : Traiter plusieurs éléments ensemble plutôt qu'individuellement
  6. Surveiller le Temps d'Exécution du Script : Garder les scripts simples et efficaces

ConclusionLien direct vers Conclusion

Maîtriser les modèles async/await dans Softyflow vous permet de créer des scripts d'automatisation de processus robustes, efficaces et maintenables. N'oubliez pas :

  • Toujours utiliser await avec les méthodes SDK
  • Gérer les erreurs avec élégance en utilisant les blocs try-catch
  • Utiliser le traitement parallèle pour les opérations indépendantes
  • Implémenter des pratiques appropriées de journalisation et de débogage
  • Tester minutieusement en utilisant l'interface de test de Softyflow

Suivre ces modèles et ces meilleures pratiques vous aidera à créer des scripts d'automatisation fiables qui s'adaptent à vos processus métier.