|
| 1 | +# Activity Handler Utility Types |
| 2 | + |
| 3 | +Cette documentation décrit les types utilitaires disponibles pour simplifier le typage des handlers d'activités. |
| 4 | + |
| 5 | +## Vue d'ensemble |
| 6 | + |
| 7 | +Au lieu de typer explicitement les paramètres et retours avec `WorkerInferInput` et `WorkerInferOutput`, vous pouvez utiliser des types utilitaires qui encapsulent toute la signature de la fonction. |
| 8 | + |
| 9 | +## Types disponibles |
| 10 | + |
| 11 | +### Pour les activités standards |
| 12 | + |
| 13 | +#### `ActivityHandler<TContract, TActivityName>` |
| 14 | + |
| 15 | +Type utilitaire pour typer un handler d'**activité globale** définie au niveau du contrat. |
| 16 | + |
| 17 | +**Exemple :** |
| 18 | + |
| 19 | +```typescript |
| 20 | +import type { ActivityHandler } from "@temporal-contract/contract"; |
| 21 | +import { myContract } from "./contract.js"; |
| 22 | + |
| 23 | +// ✅ Avec le type utilitaire |
| 24 | +const log: ActivityHandler<typeof myContract, "log"> = async ({ level, message }) => { |
| 25 | + logger[level](message); |
| 26 | +}; |
| 27 | + |
| 28 | +// ❌ Sans le type utilitaire (verbeux) |
| 29 | +const log = async ({ |
| 30 | + level, |
| 31 | + message |
| 32 | +}: WorkerInferInput<typeof myContract.activities.log>): Promise<WorkerInferOutput<typeof myContract.activities.log>> => { |
| 33 | + logger[level](message); |
| 34 | +}; |
| 35 | +``` |
| 36 | + |
| 37 | +#### `WorkflowActivityHandler<TContract, TWorkflowName, TActivityName>` |
| 38 | + |
| 39 | +Type utilitaire pour typer un handler d'**activité spécifique à un workflow**. |
| 40 | + |
| 41 | +**Exemple :** |
| 42 | + |
| 43 | +```typescript |
| 44 | +import type { WorkflowActivityHandler } from "@temporal-contract/contract"; |
| 45 | +import { myContract } from "./contract.js"; |
| 46 | + |
| 47 | +// ✅ Avec le type utilitaire |
| 48 | +const processPayment: WorkflowActivityHandler< |
| 49 | + typeof myContract, |
| 50 | + "processOrder", |
| 51 | + "processPayment" |
| 52 | +> = async ({ customerId, amount }) => { |
| 53 | + // TypeScript infère automatiquement les types de customerId et amount |
| 54 | + return { |
| 55 | + transactionId: "txn123", |
| 56 | + status: "success" as const, |
| 57 | + paidAmount: amount, |
| 58 | + }; |
| 59 | +}; |
| 60 | + |
| 61 | +// ❌ Sans le type utilitaire (très verbeux) |
| 62 | +const processPayment = async ({ |
| 63 | + customerId, |
| 64 | + amount, |
| 65 | +}: WorkerInferInput<typeof myContract.workflows.processOrder.activities.processPayment>): Promise<WorkerInferOutput<typeof myContract.workflows.processOrder.activities.processPayment>> => { |
| 66 | + // ... |
| 67 | +}; |
| 68 | +``` |
| 69 | + |
| 70 | +### Pour les activités avec pattern Result/Future (@swan-io/boxed) |
| 71 | + |
| 72 | +#### `BoxedActivityHandler<TContract, TActivityName>` |
| 73 | + |
| 74 | +Type utilitaire pour typer un handler d'**activité globale** utilisant le pattern Result/Future. |
| 75 | + |
| 76 | +**Exemple :** |
| 77 | + |
| 78 | +```typescript |
| 79 | +import { Future, Result } from "@swan-io/boxed"; |
| 80 | +import type { BoxedActivityHandler } from "@temporal-contract/worker-boxed"; |
| 81 | +import { myContract } from "./contract.js"; |
| 82 | + |
| 83 | +const log: BoxedActivityHandler<typeof myContract, "log"> = ({ |
| 84 | + level, |
| 85 | + message, |
| 86 | +}: { |
| 87 | + level: string; |
| 88 | + message: string; |
| 89 | +}) => { |
| 90 | + return Future.make((resolve) => { |
| 91 | + logger[level](message); |
| 92 | + resolve(Result.Ok(undefined)); |
| 93 | + }); |
| 94 | +}; |
| 95 | +``` |
| 96 | + |
| 97 | +**Note :** Pour les activités boxed, vous devez toujours annoter explicitement les paramètres car TypeScript n'infère pas les paramètres des fonctions de rappel. |
| 98 | + |
| 99 | +#### `BoxedWorkflowActivityHandler<TContract, TWorkflowName, TActivityName>` |
| 100 | + |
| 101 | +Type utilitaire pour typer un handler d'**activité spécifique à un workflow** utilisant le pattern Result/Future. |
| 102 | + |
| 103 | +**Exemple :** |
| 104 | + |
| 105 | +```typescript |
| 106 | +import { Future, Result } from "@swan-io/boxed"; |
| 107 | +import type { BoxedWorkflowActivityHandler } from "@temporal-contract/worker-boxed"; |
| 108 | +import { myContract } from "./contract.js"; |
| 109 | + |
| 110 | +const processPayment: BoxedWorkflowActivityHandler< |
| 111 | + typeof myContract, |
| 112 | + "processOrder", |
| 113 | + "processPayment" |
| 114 | +> = ({ customerId, amount }: { customerId: string; amount: number }) => { |
| 115 | + return Future.make((resolve) => { |
| 116 | + // Simulate payment processing |
| 117 | + const transactionId = `txn-${Date.now()}`; |
| 118 | + |
| 119 | + resolve( |
| 120 | + Result.Ok({ |
| 121 | + transactionId, |
| 122 | + status: "success" as const, |
| 123 | + paidAmount: amount, |
| 124 | + }) |
| 125 | + ); |
| 126 | + }); |
| 127 | +}; |
| 128 | +``` |
| 129 | + |
| 130 | +## Avantages |
| 131 | + |
| 132 | +✅ **Code plus concis** : Moins de verbosité, plus lisible |
| 133 | + |
| 134 | +✅ **Maintenabilité** : Le type de retour est automatiquement déduit du contrat |
| 135 | + |
| 136 | +✅ **Type safety** : TypeScript vérifie que la signature correspond au contrat |
| 137 | + |
| 138 | +✅ **DX améliorée** : Autocomplétion et IntelliSense fonctionnent parfaitement |
| 139 | + |
| 140 | +## Exemples complets |
| 141 | + |
| 142 | +### Activités standards (basic-order-processing) |
| 143 | + |
| 144 | +```typescript |
| 145 | +import { declareActivitiesHandler } from "@temporal-contract/worker"; |
| 146 | +import type { ActivityHandler, WorkflowActivityHandler } from "@temporal-contract/contract"; |
| 147 | +import { orderProcessingContract } from "../contract.js"; |
| 148 | + |
| 149 | +// Activité globale |
| 150 | +const log: ActivityHandler<typeof orderProcessingContract, "log"> = async ({ level, message }) => { |
| 151 | + logger[level](message); |
| 152 | +}; |
| 153 | + |
| 154 | +const sendNotification: ActivityHandler< |
| 155 | + typeof orderProcessingContract, |
| 156 | + "sendNotification" |
| 157 | +> = async ({ customerId, subject, message }) => { |
| 158 | + logger.info({ customerId, subject }, `Sending notification to ${customerId}`); |
| 159 | + // ... |
| 160 | +}; |
| 161 | + |
| 162 | +// Activités du workflow |
| 163 | +const processPayment: WorkflowActivityHandler< |
| 164 | + typeof orderProcessingContract, |
| 165 | + "processOrder", |
| 166 | + "processPayment" |
| 167 | +> = async ({ customerId, amount }) => { |
| 168 | + return { |
| 169 | + transactionId: `TXN${Date.now()}`, |
| 170 | + status: "success" as const, |
| 171 | + paidAmount: amount, |
| 172 | + }; |
| 173 | +}; |
| 174 | + |
| 175 | +const reserveInventory: WorkflowActivityHandler< |
| 176 | + typeof orderProcessingContract, |
| 177 | + "processOrder", |
| 178 | + "reserveInventory" |
| 179 | +> = async (items) => { |
| 180 | + return { |
| 181 | + reserved: true, |
| 182 | + reservationId: `RES${Date.now()}`, |
| 183 | + }; |
| 184 | +}; |
| 185 | + |
| 186 | +export const activitiesHandler = declareActivitiesHandler({ |
| 187 | + contract: orderProcessingContract, |
| 188 | + activities: { |
| 189 | + log, |
| 190 | + sendNotification, |
| 191 | + processPayment, |
| 192 | + reserveInventory, |
| 193 | + }, |
| 194 | +}); |
| 195 | +``` |
| 196 | + |
| 197 | +### Activités avec Result/Future (boxed-order-processing) |
| 198 | + |
| 199 | +```typescript |
| 200 | +import { Future, Result } from "@swan-io/boxed"; |
| 201 | +import type { |
| 202 | + BoxedActivityHandler, |
| 203 | + BoxedWorkflowActivityHandler, |
| 204 | +} from "@temporal-contract/worker-boxed"; |
| 205 | +import { boxedOrderContract } from "../contract.js"; |
| 206 | + |
| 207 | +// Activité globale |
| 208 | +const log: BoxedActivityHandler<typeof boxedOrderContract, "log"> = ({ |
| 209 | + level, |
| 210 | + message, |
| 211 | +}: { |
| 212 | + level: string; |
| 213 | + message: string; |
| 214 | +}) => { |
| 215 | + return Future.make((resolve) => { |
| 216 | + logger[level](message); |
| 217 | + resolve(Result.Ok(undefined)); |
| 218 | + }); |
| 219 | +}; |
| 220 | + |
| 221 | +// Activité du workflow |
| 222 | +const processPayment: BoxedWorkflowActivityHandler< |
| 223 | + typeof boxedOrderContract, |
| 224 | + "processOrder", |
| 225 | + "processPayment" |
| 226 | +> = ({ customerId, amount }: { customerId: string; amount: number }) => { |
| 227 | + return Future.make((resolve) => { |
| 228 | + const success = Math.random() > 0.1; |
| 229 | + |
| 230 | + if (success) { |
| 231 | + resolve( |
| 232 | + Result.Ok({ |
| 233 | + transactionId: `txn-${Date.now()}`, |
| 234 | + status: "success" as const, |
| 235 | + paidAmount: amount, |
| 236 | + }) |
| 237 | + ); |
| 238 | + } else { |
| 239 | + resolve( |
| 240 | + Result.Error({ |
| 241 | + code: "PAYMENT_FAILED", |
| 242 | + message: "Payment declined", |
| 243 | + details: { customerId, amount }, |
| 244 | + }) |
| 245 | + ); |
| 246 | + } |
| 247 | + }); |
| 248 | +}; |
| 249 | +``` |
| 250 | + |
| 251 | +## Cas particuliers |
| 252 | + |
| 253 | +### Activités avec des paramètres scalaires |
| 254 | + |
| 255 | +Pour les activités dont l'input est un type scalaire (string, number, etc.) et non un objet : |
| 256 | + |
| 257 | +```typescript |
| 258 | +const releaseInventory: WorkflowActivityHandler< |
| 259 | + typeof myContract, |
| 260 | + "processOrder", |
| 261 | + "releaseInventory" |
| 262 | +> = async (reservationId) => { |
| 263 | + // reservationId est typé automatiquement comme string |
| 264 | + logger.info({ reservationId }, `Releasing inventory`); |
| 265 | +}; |
| 266 | +``` |
| 267 | + |
| 268 | +### Activités retournant void |
| 269 | + |
| 270 | +```typescript |
| 271 | +const releaseInventory: WorkflowActivityHandler< |
| 272 | + typeof myContract, |
| 273 | + "processOrder", |
| 274 | + "releaseInventory" |
| 275 | +> = async (reservationId) => { |
| 276 | + logger.info({ reservationId }, `Releasing inventory`); |
| 277 | + // Pas besoin de return, le type de retour est automatiquement Promise<void> |
| 278 | +}; |
| 279 | +``` |
| 280 | + |
| 281 | +## Tests |
| 282 | + |
| 283 | +Des tests complets sont disponibles dans `/packages/contract/src/types.spec.ts` démontrant l'utilisation de ces types : |
| 284 | + |
| 285 | +```typescript |
| 286 | +describe("Activity Handler Utility Types", () => { |
| 287 | + it("should correctly type a global activity handler", async () => { |
| 288 | + const contract = { |
| 289 | + taskQueue: "test", |
| 290 | + activities: { |
| 291 | + log: { |
| 292 | + input: z.object({ level: z.string(), message: z.string() }), |
| 293 | + output: z.void(), |
| 294 | + }, |
| 295 | + }, |
| 296 | + workflows: {}, |
| 297 | + } satisfies ContractDefinition; |
| 298 | + |
| 299 | + const log: ActivityHandler<typeof contract, "log"> = async ({ level, message }) => { |
| 300 | + expect(level).toBe("info"); |
| 301 | + expect(message).toBe("test"); |
| 302 | + }; |
| 303 | + |
| 304 | + await log({ level: "info", message: "test" }); |
| 305 | + }); |
| 306 | + |
| 307 | + it("should correctly type a workflow-specific activity handler", async () => { |
| 308 | + // Voir types.spec.ts pour l'exemple complet |
| 309 | + }); |
| 310 | +}); |
| 311 | +``` |
| 312 | + |
| 313 | +## Références |
| 314 | + |
| 315 | +- [Types contract](/packages/contract/src/types.ts) |
| 316 | +- [Tests types](/packages/contract/src/types.spec.ts) |
| 317 | +- [Exemple basic-order-processing](/samples/basic-order-processing/src/activities/index.ts) |
| 318 | +- [Exemple boxed-order-processing](/samples/boxed-order-processing/src/activities/index.ts) |
0 commit comments