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 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 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/Await
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è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)
// ✅ 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)
// ✅ 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 Softyflow
1. Opérations de Base de Données
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 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'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 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 Softyflow
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 Performances
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é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 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 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és
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 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 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 Éviter
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 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 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 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ébogage
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é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 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 Performance
- Minimiser les Requêtes de Base de Données : Utiliser l'agrégation et les opérations en masse si possible
- Utiliser le Traitement Parallèle : Exécuter les opérations indépendantes simultanément
- Implémenter des Délais d'Expiration : Définir des délais d'expiration raisonnables pour les appels API externes
- Mettre en Cache les Données Fréquemment Utilisées : Stocker les données couramment consultées dans les variables
- Opérations par Lots : Traiter plusieurs éléments ensemble plutôt qu'individuellement
- Surveiller le Temps d'Exécution du Script : Garder les scripts simples et efficaces
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
awaitavec 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.