@djibdjib/scenario
v1.0.1
Published
A TypeScript library for executing sequences of functions with rollback capability, retry, parallel execution, and more (Saga pattern)
Maintainers
Readme
Scenario
Une bibliothèque TypeScript avancée pour exécuter des séquences de fonctions avec capacité de rollback automatique (Pattern Saga).
🚀 Fonctionnalités
✅ Rollback automatique - Annulation automatique en cas d'erreur
✅ Steps parallèles - Exécution simultanée de plusieurs étapes
✅ Retry & Timeout - Nouvelle tentative automatique et timeout configurable
✅ Conditional steps - Étapes conditionnelles basées sur le contexte
✅ Hooks/Events - Écoutez les événements du scenario
✅ Validation Zod - Validation du contexte avant exécution
✅ Nested scenarios - Réutilisation de sous-scenarios
✅ Scenario Builder - API fluide pour construire des scenarios
✅ Streaming - Résultats en temps réel avec AsyncGenerator
✅ TypeScript strict - Types génériques complets
✅ Métriques - Durées d'exécution détaillées
Installation
npm install @your-org/scenarioUtilisation rapide
import { Scenario } from "@your-org/scenario";
const result = await Scenario.exec(
"user-registration",
[
{
name: "Créer utilisateur",
execute: async (ctx) => {
return { userId: 123, username: ctx.username };
},
rollback: async () => {
// Supprimer l'utilisateur
},
},
{
name: "Envoyer email",
execute: async (ctx) => {
return { emailSent: true };
},
rollback: async () => {
// Annuler l'envoi (optionnel)
},
},
],
{ username: "Alice", email: "[email protected]" },
);
if (result.success) {
console.log("✅ Inscription réussie:", result.data);
} else {
console.log("❌ Erreur:", result.error);
}📚 Exemples détaillés
1. Utilisation basique avec contexte
const result = await Scenario.exec(
"create-user",
[
{
name: "Créer utilisateur",
execute: async (ctx) => {
return {
userId: 123,
username: ctx.username,
};
},
rollback: async () => {},
},
{
name: "Envoyer email",
execute: async (ctx) => {
return { emailSent: true };
},
rollback: async () => {},
},
],
{ username: "Alice", email: "[email protected]" },
);2. Retry automatique avec backoff exponentiel
let attemptCount = 0;
const result = await Scenario.exec(
"test-retry",
[
{
name: "API instable",
execute: async (ctx) => {
attemptCount++;
console.log(`🔄 Tentative ${attemptCount}/3`);
if (attemptCount < 3) {
throw new Error("Service temporairement indisponible");
}
return { data: "Succès!", attempts: attemptCount };
},
rollback: async () => {
console.log("🔙 Rollback exécuté");
},
retry: {
attempts: 3, // 3 tentatives max
delay: 500, // 500ms de délai initial
exponential: true, // backoff exponentiel (500ms, 1000ms, 2000ms...)
},
timeout: 5000, // timeout de 5 secondes par tentative
},
],
{},
);3. Rollback automatique
Quand une étape échoue, toutes les étapes précédentes sont annulées automatiquement dans l'ordre inverse :
const result = await Scenario.exec(
"payment-flow",
[
{
name: "Réserver stock",
execute: async (ctx) => {
console.log("📦 Stock réservé: 2 articles");
return { stockReserved: true, items: 2 };
},
rollback: async (ctx) => {
console.log("🔄 Rollback: Libération du stock");
},
},
{
name: "Débiter compte",
execute: async (ctx) => {
console.log("💳 Compte débité: 50€");
return { charged: true, amount: 50 };
},
rollback: async (ctx) => {
console.log("🔄 Rollback: Remboursement de 50€");
},
},
{
name: "Envoyer confirmation",
execute: async (ctx) => {
console.log("❌ Erreur: Service email indisponible");
throw new Error("Service email indisponible");
},
rollback: async (ctx) => {
console.log("🔄 Rollback: Annulation email");
},
},
],
{},
);
if (!result.success) {
console.log("⚠️ Transaction échouée, rollback effectué");
}4. Steps conditionnels
const isPremium = Math.random() > 0.5;
const result = await Scenario.exec(
"conditional",
[
{
name: "Vérifier statut",
execute: async (ctx) => {
console.log(`👤 Utilisateur ${ctx.isPremium ? "Premium ⭐" : "Standard"}`);
return {};
},
rollback: async () => {},
},
{
name: "Bonus premium",
execute: async (ctx) => {
console.log("💎 Application du bonus premium: 50€");
return { bonus: 50 };
},
rollback: async () => {},
condition: (ctx) => ctx.isPremium === true, // Exécuté seulement si premium
},
{
name: "Offre standard",
execute: async (ctx) => {
console.log("📢 Proposition d'upgrade Premium");
return { upgradeOffer: true, bonus: 0 };
},
rollback: async () => {},
condition: (ctx) => ctx.isPremium !== true, // Exécuté seulement si non premium
},
],
{ isPremium },
);5. Exécution parallèle
Utilisez un tableau de steps pour les exécuter en parallèle :
const result = await Scenario.exec(
"parallel-fetch",
[
[
// Ces 3 steps s'exécutent en parallèle
{
name: "Fetch utilisateur",
execute: async (ctx) => ({ user: "Alice" }),
rollback: async () => {},
},
{
name: "Fetch commandes",
execute: async (ctx) => ({ orders: 5 }),
rollback: async () => {},
},
{
name: "Fetch produits",
execute: async (ctx) => ({ products: 12 }),
rollback: async () => {},
},
],
],
{},
);
console.log(result.data); // { step1: { step1: { user: "Alice" }, step2: { orders: 5 }, step3: { products: 12 } } }6. Hooks & Événements
const result = await Scenario.exec(
"test-hooks",
[
{
name: "Étape 1",
execute: async (ctx) => {
await new Promise((resolve) => setTimeout(resolve, 200));
return { step1: true };
},
rollback: async () => {},
},
{
name: "Étape 2",
execute: async (ctx) => {
await new Promise((resolve) => setTimeout(resolve, 200));
return { step2: true };
},
rollback: async () => {},
},
],
{},
{
hooks: {
onStepStart: (index, stepName) => {
console.log(`▶️ Début: ${stepName}`);
},
onStepComplete: (index, stepName) => {
console.log(`✅ Fin: ${stepName}`);
},
onFlowComplete: () => {
console.log("🎉 Scenario terminé!");
},
},
},
);7. Validation du contexte avec Zod
import { z } from "zod";
const userSchema = z.object({
name: z.string().min(3),
email: z.string().email(),
age: z.number().min(18),
});
const result = await Scenario.exec(
"test-validation",
[
{
name: "Valider utilisateur",
execute: async (ctx) => {
return {
name: ctx.name,
email: ctx.email,
age: ctx.age,
};
},
rollback: async () => {},
zodSchema: userSchema,
onValidationError: (error) => {
console.log(`⚠️ Validation: ${error.issues[0]?.message}`);
},
},
],
{ name: "Alice", email: "[email protected]", age: 25 },
{
hooks: {
onValidationError: (index, stepName, error) => {
console.log(`🔴 ${stepName} - ${error.issues[0]?.message}`);
},
},
},
);8. Builder API (fluent)
const workflow = Scenario.builder()
.step({
name: "Init",
execute: async (ctx) => {
await new Promise((resolve) => setTimeout(resolve, 100));
return { initialized: true };
},
rollback: async () => {},
})
.step({
name: "Process",
execute: async (ctx) => {
await new Promise((resolve) => setTimeout(resolve, 100));
return { processed: true };
},
rollback: async () => {},
})
.step({
name: "Finalize",
execute: async (ctx) => {
await new Promise((resolve) => setTimeout(resolve, 100));
return { finalized: true };
},
rollback: async () => {},
})
.onStepComplete((index, stepName) => {
console.log(`✓ ${stepName}`);
})
.build();
const result = await workflow.execute({});9. Streaming (AsyncGenerator)
Recevez les résultats en temps réel :
const steps = [
{
name: "Étape 1",
execute: async (ctx) => {
await new Promise((resolve) => setTimeout(resolve, 300));
return { step1: true };
},
rollback: async () => {},
},
{
name: "Étape 2",
execute: async (ctx) => {
await new Promise((resolve) => setTimeout(resolve, 300));
return { step2: true };
},
rollback: async () => {},
},
{
name: "Étape 3",
execute: async (ctx) => {
await new Promise((resolve) => setTimeout(resolve, 300));
return { step3: true };
},
rollback: async () => {},
},
];
for await (const update of Scenario.stream("test-streaming", steps, {})) {
const stepName = steps[update.stepIndex]?.name;
if (update.success) {
console.log(`✅ ${stepName} terminé`);
} else {
console.log(`❌ ${stepName} erreur`);
}
}10. Scenarios imbriqués (réutilisables)
Créez des sous-scenarios réutilisables :
const authenticateUser = Scenario.create([
{
name: "Vérifier token",
execute: async (ctx) => ({ tokenValid: true }),
rollback: async () => {},
},
{
name: "Charger profil",
execute: async (ctx) => ({ userId: 123, username: "Alice" }),
rollback: async () => {},
},
]);
const result = await Scenario.exec(
"test-nested",
[
authenticateUser, // Réutilisation du sous-scenario
{
name: "Action métier",
execute: async (ctx) => ({
action: "completed",
userId: ctx.userId,
username: ctx.username,
}),
rollback: async () => {},
},
],
{},
);
if (result.success && result.data) {
console.log(
`✅ Utilisateur: ${result.data?.step1?.step2?.username} (ID: ${result.data?.step1?.step2?.userId})`,
);
}📖 API
Scenario.exec()
Scenario.exec<T>(
name: string,
steps: ScenarioStep[],
initialContext?: any,
options?: ScenarioOptions
): Promise<ScenarioResult<T>>ScenarioStep
interface ScenarioStep {
name?: string;
execute: (context: any) => Promise<any> | any;
rollback?: (context: any, result?: any) => Promise<void> | void;
retry?: {
attempts: number;
delay?: number;
exponential?: boolean;
};
timeout?: number;
condition?: (context: any) => boolean | Promise<boolean>;
zodSchema?: ZodSchema;
onValidationError?: (error: z.ZodError, context: any) => void;
}ScenarioOptions
interface ScenarioOptions {
hooks?: {
onStepStart?: (stepIndex: number, stepName: string, context: any) => void;
onStepComplete?: (stepIndex: number, stepName: string, result: any) => void;
onStepError?: (stepIndex: number, stepName: string, error: Error) => void;
onValidationError?: (
stepIndex: number,
stepName: string,
error: z.ZodError,
context: any,
) => void;
onRollbackStart?: (stepIndex: number, stepName: string) => void;
onRollbackComplete?: (stepIndex: number, stepName: string) => void;
onFlowComplete?: (result: ScenarioResult) => void;
};
rollbackMode?: "sync" | "async"; // défaut: 'sync'
timeout?: number;
}ScenarioResult
interface ScenarioResult<T = any> {
success: boolean;
data?: T;
error?: Error;
completedSteps: number;
metrics: {
startTime: number;
endTime: number;
duration: number;
stepDurations: number[];
};
}Autres méthodes
Scenario.builder(name?)- API fluide pour construire un scenarioScenario.create(steps)- Créer un sous-scenario réutilisableScenario.stream(name, steps, context?, options?)- Exécution en streaming
🎯 Cas d'usage
- Transactions distribuées - Gérer les opérations multi-services avec rollback
- Workflows métier - Orchestrer des processus complexes
- Pipelines de données - Traitement avec retry et rollback
- Intégrations API - Appels séquentiels ou parallèles avec gestion d'erreurs
- Migrations de données - Avec possibilité d'annulation
- Processus d'inscription - Avec validation et étapes conditionnelles
🔧 Configuration TypeScript
Ajoutez à votre tsconfig.json :
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true
}
}📝 License
MIT
🤝 Contributing
Les contributions sont les bienvenues ! N'hésitez pas à ouvrir une issue ou une pull request.
📦 Package
Ce package est publié sur npm sous le nom @your-org/scenario.
