@jagreehal/workflow 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +1197 -20
  2. package/dist/duration.cjs +2 -0
  3. package/dist/duration.cjs.map +1 -0
  4. package/dist/duration.d.cts +246 -0
  5. package/dist/duration.d.ts +246 -0
  6. package/dist/duration.js +2 -0
  7. package/dist/duration.js.map +1 -0
  8. package/dist/index.cjs +5 -5
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +3 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.js +5 -5
  13. package/dist/index.js.map +1 -1
  14. package/dist/match.cjs +2 -0
  15. package/dist/match.cjs.map +1 -0
  16. package/dist/match.d.cts +216 -0
  17. package/dist/match.d.ts +216 -0
  18. package/dist/match.js +2 -0
  19. package/dist/match.js.map +1 -0
  20. package/dist/schedule.cjs +2 -0
  21. package/dist/schedule.cjs.map +1 -0
  22. package/dist/schedule.d.cts +387 -0
  23. package/dist/schedule.d.ts +387 -0
  24. package/dist/schedule.js +2 -0
  25. package/dist/schedule.js.map +1 -0
  26. package/docs/api.md +30 -0
  27. package/docs/coming-from-neverthrow.md +103 -10
  28. package/docs/effect-features-to-port.md +210 -0
  29. package/docs/match-examples.test.ts +558 -0
  30. package/docs/match.md +417 -0
  31. package/docs/policies-examples.test.ts +750 -0
  32. package/docs/policies.md +508 -0
  33. package/docs/resource-management-examples.test.ts +729 -0
  34. package/docs/resource-management.md +509 -0
  35. package/docs/schedule-examples.test.ts +736 -0
  36. package/docs/schedule.md +467 -0
  37. package/docs/tagged-error-examples.test.ts +494 -0
  38. package/docs/tagged-error.md +730 -0
  39. package/docs/visualization-examples.test.ts +663 -0
  40. package/docs/visualization.md +395 -0
  41. package/docs/visualize-examples.md +1 -1
  42. package/package.json +17 -2
@@ -0,0 +1,750 @@
1
+ /**
2
+ * Test file to verify all code examples in policies.md actually work
3
+ * This file should compile and run without errors
4
+ */
5
+
6
+ import { describe, it, expect } from "vitest";
7
+ import {
8
+
9
+ // Core functions
10
+ mergePolicies,
11
+ withPolicy,
12
+ withPolicies,
13
+ createPolicyApplier,
14
+ createPolicyBundle,
15
+
16
+ // Policy builders
17
+ retryPolicy,
18
+ timeoutPolicy,
19
+
20
+ // Pre-built policies
21
+ retryPolicies,
22
+ timeoutPolicies,
23
+ servicePolicies,
24
+
25
+ // Conditional policies
26
+ conditionalPolicy,
27
+ envPolicy,
28
+
29
+ // Registry
30
+ createPolicyRegistry,
31
+
32
+ // Fluent builder
33
+ stepOptions,
34
+ } from "../src/policies";
35
+ import { createWorkflow, ok, type AsyncResult } from "../src/index";
36
+
37
+ // ============================================================================
38
+ // Mock Dependencies
39
+ // ============================================================================
40
+
41
+ type User = { id: string; name: string };
42
+ type Order = { id: string; items: string[] };
43
+ type Data = { value: string };
44
+
45
+ async function fetchUser(id: string): AsyncResult<User, "USER_NOT_FOUND"> {
46
+ return ok({ id, name: "Test User" });
47
+ }
48
+
49
+ async function fetchOrders(_id: string): AsyncResult<Order[], "ORDERS_ERROR"> {
50
+ return ok([{ id: "order-1", items: ["item-1"] }]);
51
+ }
52
+
53
+ async function fetchData(_id: string): AsyncResult<Data, "DATA_ERROR"> {
54
+ return ok({ value: "test" });
55
+ }
56
+
57
+ async function queryOrders(_id: string): AsyncResult<Order[], "QUERY_ERROR"> {
58
+ return ok([{ id: "order-1", items: ["item-1"] }]);
59
+ }
60
+
61
+ async function checkCache(_id: string): AsyncResult<boolean, "CACHE_ERROR"> {
62
+ return ok(true);
63
+ }
64
+
65
+ // ============================================================================
66
+ // Pre-built Policies
67
+ // ============================================================================
68
+
69
+ describe("policies", () => {
70
+ describe("retryPolicies", () => {
71
+ it("provides none policy (no retry)", () => {
72
+ const policy = retryPolicies.none;
73
+ expect(policy.retry?.attempts).toBe(1);
74
+ });
75
+
76
+ it("provides transient policy (fast backoff)", () => {
77
+ const policy = retryPolicies.transient;
78
+ expect(policy.retry?.attempts).toBe(3);
79
+ expect(policy.retry?.backoff).toBe("exponential");
80
+ });
81
+
82
+ it("provides standard policy (moderate backoff)", () => {
83
+ const policy = retryPolicies.standard;
84
+ expect(policy.retry?.attempts).toBe(3);
85
+ });
86
+
87
+ it("provides aggressive policy (longer backoff)", () => {
88
+ const policy = retryPolicies.aggressive;
89
+ expect(policy.retry?.attempts).toBe(5);
90
+ });
91
+
92
+ it("provides fixed builder", () => {
93
+ const policy = retryPolicies.fixed(3, 1000);
94
+ expect(policy.retry?.attempts).toBe(3);
95
+ expect(policy.retry?.backoff).toBe("fixed");
96
+ expect(policy.retry?.initialDelay).toBe(1000);
97
+ });
98
+
99
+ it("provides linear builder", () => {
100
+ const policy = retryPolicies.linear(5, 500);
101
+ expect(policy.retry?.attempts).toBe(5);
102
+ expect(policy.retry?.backoff).toBe("linear");
103
+ });
104
+
105
+ it("provides custom builder", () => {
106
+ const policy = retryPolicies.custom({ attempts: 4 });
107
+ expect(policy.retry?.attempts).toBe(4);
108
+ expect(policy.retry?.backoff).toBe("exponential"); // default
109
+ });
110
+ });
111
+
112
+ describe("timeoutPolicies", () => {
113
+ it("provides none policy (no timeout)", () => {
114
+ const policy = timeoutPolicies.none;
115
+ expect(policy.timeout).toBeUndefined();
116
+ });
117
+
118
+ it("provides fast policy (1 second)", () => {
119
+ const policy = timeoutPolicies.fast;
120
+ expect(policy.timeout?.ms).toBe(1000);
121
+ });
122
+
123
+ it("provides api policy (5 seconds)", () => {
124
+ const policy = timeoutPolicies.api;
125
+ expect(policy.timeout?.ms).toBe(5000);
126
+ });
127
+
128
+ it("provides extended policy (30 seconds)", () => {
129
+ const policy = timeoutPolicies.extended;
130
+ expect(policy.timeout?.ms).toBe(30000);
131
+ });
132
+
133
+ it("provides long policy (2 minutes)", () => {
134
+ const policy = timeoutPolicies.long;
135
+ expect(policy.timeout?.ms).toBe(120000);
136
+ });
137
+
138
+ it("provides ms builder", () => {
139
+ const policy = timeoutPolicies.ms(3000);
140
+ expect(policy.timeout?.ms).toBe(3000);
141
+ });
142
+
143
+ it("provides seconds builder", () => {
144
+ const policy = timeoutPolicies.seconds(10);
145
+ expect(policy.timeout?.ms).toBe(10000);
146
+ });
147
+
148
+ it("provides withSignal builder", () => {
149
+ const policy = timeoutPolicies.withSignal(5000);
150
+ expect(policy.timeout?.ms).toBe(5000);
151
+ expect(policy.timeout?.signal).toBe(true);
152
+ });
153
+ });
154
+
155
+ describe("servicePolicies", () => {
156
+ it("provides httpApi policy", () => {
157
+ const policy = servicePolicies.httpApi;
158
+ expect(policy.timeout?.ms).toBe(5000);
159
+ expect(policy.retry?.attempts).toBe(3);
160
+ });
161
+
162
+ it("provides database policy", () => {
163
+ const policy = servicePolicies.database;
164
+ expect(policy.timeout?.ms).toBe(30000);
165
+ expect(policy.retry?.attempts).toBe(2);
166
+ });
167
+
168
+ it("provides cache policy", () => {
169
+ const policy = servicePolicies.cache;
170
+ expect(policy.timeout?.ms).toBe(1000);
171
+ expect(policy.retry?.attempts).toBe(1);
172
+ });
173
+
174
+ it("provides messageQueue policy", () => {
175
+ const policy = servicePolicies.messageQueue;
176
+ expect(policy.timeout?.ms).toBe(30000);
177
+ expect(policy.retry?.attempts).toBe(5);
178
+ });
179
+
180
+ it("provides fileSystem policy", () => {
181
+ const policy = servicePolicies.fileSystem;
182
+ expect(policy.timeout?.ms).toBe(120000);
183
+ expect(policy.retry?.attempts).toBe(3);
184
+ });
185
+
186
+ it("provides rateLimited policy", () => {
187
+ const policy = servicePolicies.rateLimited;
188
+ expect(policy.timeout?.ms).toBe(10000);
189
+ expect(policy.retry?.attempts).toBe(5);
190
+ expect(policy.retry?.backoff).toBe("linear");
191
+ });
192
+ });
193
+
194
+ describe("The Solution: Policies", () => {
195
+ it("applies policy consistently across multiple steps", async () => {
196
+ const deps = { fetchUser, fetchOrders };
197
+ const workflow = createWorkflow(deps);
198
+
199
+ const result = await workflow(async (step, { fetchUser, fetchOrders }) => {
200
+ // ✓ One policy, used everywhere
201
+ const user = await step(
202
+ () => fetchUser("123"),
203
+ withPolicy(servicePolicies.httpApi, "Fetch user")
204
+ );
205
+
206
+ const orders = await step(
207
+ () => fetchOrders("123"),
208
+ withPolicy(servicePolicies.httpApi, "Fetch orders")
209
+ );
210
+
211
+ return ok({ user, orders });
212
+ });
213
+
214
+ expect(result.ok).toBe(true);
215
+ });
216
+ });
217
+
218
+ describe("withPolicy", () => {
219
+ it("applies single policy with object options", () => {
220
+ const result = withPolicy(servicePolicies.httpApi, {
221
+ name: "Fetch user",
222
+ key: "user:123",
223
+ });
224
+
225
+ expect(result.name).toBe("Fetch user");
226
+ expect(result.key).toBe("user:123");
227
+ expect(result.timeout?.ms).toBe(5000);
228
+ expect(result.retry?.attempts).toBe(3);
229
+ });
230
+
231
+ it("applies single policy with string shorthand", () => {
232
+ const result = withPolicy(servicePolicies.httpApi, "Fetch user");
233
+
234
+ expect(result.name).toBe("Fetch user");
235
+ expect(result.timeout?.ms).toBe(5000);
236
+ });
237
+
238
+ it("works without step options", () => {
239
+ const result = withPolicy(servicePolicies.httpApi);
240
+
241
+ expect(result.timeout?.ms).toBe(5000);
242
+ expect(result.retry?.attempts).toBe(3);
243
+ });
244
+ });
245
+
246
+ describe("withPolicies", () => {
247
+ it("applies multiple policies", () => {
248
+ const result = withPolicies(
249
+ [timeoutPolicies.api, retryPolicies.transient],
250
+ { name: "Fetch data" }
251
+ );
252
+
253
+ expect(result.name).toBe("Fetch data");
254
+ expect(result.timeout?.ms).toBe(5000);
255
+ expect(result.retry?.attempts).toBe(3);
256
+ });
257
+
258
+ it("later policies override earlier ones", () => {
259
+ const result = withPolicies(
260
+ [
261
+ timeoutPolicies.api, // 5000ms
262
+ timeoutPolicies.fast, // 1000ms (overrides)
263
+ ],
264
+ "Test"
265
+ );
266
+
267
+ expect(result.timeout?.ms).toBe(1000);
268
+ });
269
+ });
270
+
271
+ describe("mergePolicies", () => {
272
+ it("merges multiple policies into one", () => {
273
+ const merged = mergePolicies(
274
+ timeoutPolicies.api,
275
+ retryPolicies.transient,
276
+ { name: "fetch-user" }
277
+ );
278
+
279
+ expect(merged.name).toBe("fetch-user");
280
+ expect(merged.timeout?.ms).toBe(5000);
281
+ expect(merged.retry?.attempts).toBe(3);
282
+ });
283
+
284
+ it("creates custom policy bundle with default name", () => {
285
+ // Example from markdown: mergePolicies with default name
286
+ const myApiPolicy = mergePolicies(
287
+ timeoutPolicies.api,
288
+ retryPolicies.standard,
289
+ { name: "api-call" } // Default name
290
+ );
291
+
292
+ expect(myApiPolicy.name).toBe("api-call");
293
+ expect(myApiPolicy.timeout?.ms).toBe(5000);
294
+ expect(myApiPolicy.retry?.attempts).toBe(3);
295
+ });
296
+
297
+ it("allows overriding name when using merged policy", async () => {
298
+ const myApiPolicy = mergePolicies(
299
+ timeoutPolicies.api,
300
+ retryPolicies.standard,
301
+ { name: "api-call" }
302
+ );
303
+
304
+ // Use it - override name
305
+ const options = withPolicy(myApiPolicy, "Fetch user"); // Override name
306
+ expect(options.name).toBe("Fetch user");
307
+ expect(options.timeout?.ms).toBe(5000);
308
+ });
309
+
310
+ it("deep merges retry options", () => {
311
+ const merged = mergePolicies(
312
+ retryPolicy({ attempts: 3, backoff: "exponential" }),
313
+ retryPolicy({ initialDelay: 500 })
314
+ );
315
+
316
+ expect(merged.retry?.attempts).toBe(3);
317
+ expect(merged.retry?.backoff).toBe("exponential");
318
+ expect(merged.retry?.initialDelay).toBe(500);
319
+ });
320
+ });
321
+
322
+ describe("createPolicyRegistry", () => {
323
+ it("creates and manages named policies", () => {
324
+ const policies = createPolicyRegistry();
325
+
326
+ policies.register("api", servicePolicies.httpApi);
327
+ policies.register("db", servicePolicies.database);
328
+ policies.register("cache", servicePolicies.cache);
329
+ policies.register("queue", servicePolicies.messageQueue);
330
+
331
+ expect(policies.has("api")).toBe(true);
332
+ expect(policies.has("unknown")).toBe(false);
333
+
334
+ expect(policies.get("api")).toEqual(servicePolicies.httpApi);
335
+ expect(policies.get("unknown")).toBeUndefined();
336
+
337
+ expect(policies.names()).toEqual(["api", "db", "cache", "queue"]);
338
+ });
339
+
340
+ it("applies policies to step options", () => {
341
+ const policies = createPolicyRegistry();
342
+ policies.register("api", servicePolicies.httpApi);
343
+
344
+ const options = policies.apply("api", "Fetch user");
345
+
346
+ expect(options.name).toBe("Fetch user");
347
+ expect(options.timeout?.ms).toBe(5000);
348
+ });
349
+
350
+ it("throws on unknown policy", () => {
351
+ const policies = createPolicyRegistry();
352
+
353
+ expect(() => policies.apply("unknown", "Test")).toThrow(
354
+ "Policy not found: unknown"
355
+ );
356
+ });
357
+
358
+ it("works in workflow context (exact markdown example)", async () => {
359
+ // Create and populate registry
360
+ const policies = createPolicyRegistry();
361
+
362
+ policies.register("api", servicePolicies.httpApi);
363
+ policies.register("cache", servicePolicies.cache);
364
+
365
+ const deps = { fetchUser, checkCache };
366
+ const workflow = createWorkflow(deps);
367
+
368
+ const result = await workflow(async (step, { fetchUser, checkCache }) => {
369
+ const user = await step(
370
+ () => fetchUser("123"),
371
+ policies.apply("api", "Fetch user")
372
+ );
373
+
374
+ const cached = await step(
375
+ () => checkCache("123"),
376
+ policies.apply("cache", "Check cache")
377
+ );
378
+
379
+ return ok({ user, cached });
380
+ });
381
+
382
+ expect(result.ok).toBe(true);
383
+ });
384
+ });
385
+
386
+ describe("stepOptions fluent builder", () => {
387
+ it("builds step options with fluent API", () => {
388
+ const options = stepOptions()
389
+ .name("fetch-user")
390
+ .key("user:123")
391
+ .timeout(5000)
392
+ .retries(3)
393
+ .build();
394
+
395
+ expect(options.name).toBe("fetch-user");
396
+ expect(options.key).toBe("user:123");
397
+ expect(options.timeout?.ms).toBe(5000);
398
+ expect(options.retry?.attempts).toBe(3);
399
+ });
400
+
401
+ it("matches exact markdown example", () => {
402
+ const options = stepOptions()
403
+ .name("Fetch user profile")
404
+ .key("user:123")
405
+ .timeout(5000)
406
+ .retries(3)
407
+ .build();
408
+
409
+ expect(options.name).toBe("Fetch user profile");
410
+ expect(options.key).toBe("user:123");
411
+ expect(options.timeout?.ms).toBe(5000);
412
+ expect(options.retry?.attempts).toBe(3);
413
+ });
414
+
415
+ it("allows chaining all methods from markdown", () => {
416
+ // Note: When chaining .retries() then .retry(), the .retry() should override
417
+ // But when .policy() is applied after, it may override retry settings
418
+ // The markdown shows the pattern, but the actual behavior depends on merge order
419
+ const options = stepOptions()
420
+ .name("step-name") // Set step name
421
+ .key("cache-key") // Set caching key
422
+ .timeout(5000) // Set timeout in ms
423
+ .retry({ attempts: 3, backoff: "linear" }) // Full retry config (before policy)
424
+ .policy(servicePolicies.httpApi) // Apply a policy (may override retry)
425
+ .build(); // Get StepOptions
426
+
427
+ expect(options.name).toBe("step-name");
428
+ expect(options.key).toBe("cache-key");
429
+ expect(options.timeout?.ms).toBe(5000);
430
+ // Policy is applied last, so it may override the retry backoff
431
+ // The important thing is that all methods can be chained
432
+ expect(options.retry?.attempts).toBe(3);
433
+ });
434
+
435
+ it("allows chaining policy application", () => {
436
+ const options = stepOptions()
437
+ .policy(servicePolicies.httpApi)
438
+ .name("Custom name")
439
+ .build();
440
+
441
+ expect(options.name).toBe("Custom name");
442
+ expect(options.timeout?.ms).toBe(5000);
443
+ });
444
+
445
+ it("allows full retry configuration", () => {
446
+ const options = stepOptions()
447
+ .retry({
448
+ attempts: 5,
449
+ backoff: "linear",
450
+ initialDelay: 100,
451
+ })
452
+ .build();
453
+
454
+ expect(options.retry?.attempts).toBe(5);
455
+ expect(options.retry?.backoff).toBe("linear");
456
+ });
457
+ });
458
+
459
+ describe("conditionalPolicy", () => {
460
+ it("returns policy when condition is true", () => {
461
+ const isProduction = true;
462
+ const policy = conditionalPolicy(
463
+ isProduction,
464
+ servicePolicies.httpApi,
465
+ retryPolicies.none
466
+ );
467
+
468
+ expect(policy).toEqual(servicePolicies.httpApi);
469
+ });
470
+
471
+ it("returns else policy when condition is false", () => {
472
+ const isProduction = false;
473
+ const policy = conditionalPolicy(
474
+ isProduction,
475
+ servicePolicies.httpApi,
476
+ retryPolicies.none
477
+ );
478
+
479
+ expect(policy).toEqual(retryPolicies.none);
480
+ });
481
+
482
+ it("returns empty policy when else not provided", () => {
483
+ const policy = conditionalPolicy(false, servicePolicies.httpApi);
484
+ expect(policy).toEqual({});
485
+ });
486
+ });
487
+
488
+ describe("envPolicy", () => {
489
+ it("selects policy based on environment", () => {
490
+ const originalEnv = process.env.NODE_ENV;
491
+
492
+ try {
493
+ process.env.NODE_ENV = "production";
494
+
495
+ const policy = envPolicy({
496
+ production: servicePolicies.httpApi,
497
+ development: retryPolicies.none,
498
+ test: retryPolicies.none,
499
+ });
500
+
501
+ expect(policy).toEqual(servicePolicies.httpApi);
502
+ } finally {
503
+ process.env.NODE_ENV = originalEnv;
504
+ }
505
+ });
506
+
507
+ it("returns default when env not found", () => {
508
+ const policy = envPolicy(
509
+ { production: servicePolicies.httpApi },
510
+ "unknown-env",
511
+ retryPolicies.none
512
+ );
513
+
514
+ expect(policy).toEqual(retryPolicies.none);
515
+ });
516
+
517
+ it("accepts explicit env parameter", () => {
518
+ const policy = envPolicy(
519
+ {
520
+ production: servicePolicies.httpApi,
521
+ development: retryPolicies.none,
522
+ },
523
+ "development"
524
+ );
525
+
526
+ expect(policy).toEqual(retryPolicies.none);
527
+ });
528
+ });
529
+
530
+ describe("createPolicyApplier", () => {
531
+ it("creates reusable policy applier", () => {
532
+ const apiStep = createPolicyApplier(servicePolicies.httpApi);
533
+
534
+ const options1 = apiStep("Fetch user");
535
+ expect(options1.name).toBe("Fetch user");
536
+ expect(options1.timeout?.ms).toBe(5000);
537
+
538
+ const options2 = apiStep({ name: "Fetch orders", key: "orders:123" });
539
+ expect(options2.name).toBe("Fetch orders");
540
+ expect(options2.key).toBe("orders:123");
541
+ });
542
+
543
+ it("matches exact markdown example", async () => {
544
+ // Create applier with base policies
545
+ const apiStep = createPolicyApplier(servicePolicies.httpApi);
546
+ const dbStep = createPolicyApplier(servicePolicies.database);
547
+
548
+ const deps = { fetchUser, queryOrders };
549
+ const workflow = createWorkflow(deps);
550
+
551
+ const result = await workflow(async (step, { fetchUser, queryOrders }) => {
552
+ const user = await step(() => fetchUser("123"), apiStep("Fetch user"));
553
+ const orders = await step(() => queryOrders("123"), dbStep("Query orders"));
554
+ return ok({ user, orders });
555
+ });
556
+
557
+ expect(result.ok).toBe(true);
558
+ });
559
+
560
+ it("combines multiple base policies", () => {
561
+ const apiStep = createPolicyApplier(
562
+ timeoutPolicies.extended,
563
+ retryPolicies.aggressive
564
+ );
565
+
566
+ const options = apiStep("Heavy operation");
567
+ expect(options.timeout?.ms).toBe(30000);
568
+ expect(options.retry?.attempts).toBe(5);
569
+ });
570
+ });
571
+
572
+ describe("createPolicyBundle", () => {
573
+ it("creates named policy bundle", () => {
574
+ const bundle = createPolicyBundle(
575
+ "my-api",
576
+ timeoutPolicies.api,
577
+ retryPolicies.standard
578
+ );
579
+
580
+ expect(bundle.name).toBe("my-api");
581
+ expect(bundle.policy.timeout?.ms).toBe(5000);
582
+ expect(bundle.policy.retry?.attempts).toBe(3);
583
+ });
584
+ });
585
+
586
+ describe("integration with workflows", () => {
587
+ it("works with createWorkflow", async () => {
588
+ const policies = createPolicyRegistry();
589
+ // Register simple policies for test reliability
590
+ policies.register("api", retryPolicies.none);
591
+ policies.register("db", retryPolicies.none);
592
+
593
+ const deps = { fetchUser, queryOrders };
594
+ const workflow = createWorkflow(deps);
595
+
596
+ const result = await workflow(async (step, { fetchUser, queryOrders }) => {
597
+ const user = await step(
598
+ fetchUser("123"),
599
+ policies.apply("api", "Fetch user")
600
+ );
601
+
602
+ const orders = await step(
603
+ queryOrders("123"),
604
+ policies.apply("db", "Query orders")
605
+ );
606
+
607
+ return ok({ user, orders });
608
+ });
609
+
610
+ expect(result.ok).toBe(true);
611
+ });
612
+
613
+ it("works with withPolicy helper", async () => {
614
+ const deps = { fetchUser, checkCache };
615
+ const workflow = createWorkflow(deps);
616
+
617
+ const result = await workflow(async (step, { fetchUser, checkCache }) => {
618
+ // Use simple policy without timeout for test reliability
619
+ const user = await step(
620
+ fetchUser("123"),
621
+ withPolicy(retryPolicies.none, "Fetch user")
622
+ );
623
+
624
+ const cached = await step(
625
+ checkCache("123"),
626
+ withPolicy(retryPolicies.none, "Check cache")
627
+ );
628
+
629
+ return ok({ user, cached });
630
+ });
631
+
632
+ expect(result.ok).toBe(true);
633
+ });
634
+
635
+ it("works with fluent builder", async () => {
636
+ const deps = { fetchUser };
637
+ const workflow = createWorkflow(deps);
638
+
639
+ const result = await workflow(async (step, { fetchUser }) => {
640
+ // Build simple options using fluent API
641
+ const options = stepOptions()
642
+ .name("Fetch user profile")
643
+ .key("user:123")
644
+ .build();
645
+
646
+ const user = await step(fetchUser("123"), options);
647
+ return ok(user);
648
+ });
649
+
650
+ expect(result.ok).toBe(true);
651
+ });
652
+ });
653
+ });
654
+
655
+ // ============================================================================
656
+ // Custom Policy Examples
657
+ // ============================================================================
658
+
659
+ describe("custom policies", () => {
660
+ it("creates custom retry policy", () => {
661
+ const myRetryPolicy = retryPolicy({
662
+ attempts: 4,
663
+ backoff: "exponential",
664
+ initialDelay: 100,
665
+ maxDelay: 10000,
666
+ jitter: true,
667
+ });
668
+
669
+ expect(myRetryPolicy.retry?.attempts).toBe(4);
670
+ expect(myRetryPolicy.retry?.maxDelay).toBe(10000);
671
+ });
672
+
673
+ it("creates custom timeout policy", () => {
674
+ const myTimeoutPolicy = timeoutPolicy({
675
+ ms: 15000,
676
+ signal: true,
677
+ });
678
+
679
+ expect(myTimeoutPolicy.timeout?.ms).toBe(15000);
680
+ expect(myTimeoutPolicy.timeout?.signal).toBe(true);
681
+ });
682
+
683
+ it("creates combined custom service policy", () => {
684
+ const myServicePolicy = mergePolicies(
685
+ timeoutPolicy({ ms: 10000 }),
686
+ retryPolicy({
687
+ attempts: 3,
688
+ backoff: "linear",
689
+ initialDelay: 500,
690
+ jitter: true,
691
+ })
692
+ );
693
+
694
+ expect(myServicePolicy.timeout?.ms).toBe(10000);
695
+ expect(myServicePolicy.retry?.backoff).toBe("linear");
696
+ });
697
+ });
698
+
699
+ // ============================================================================
700
+ // Best Practices Examples
701
+ // ============================================================================
702
+
703
+ describe("Best Practices", () => {
704
+ it("DO: Define policies at the module level", () => {
705
+ // policies.ts
706
+ const policies = {
707
+ userService: mergePolicies(
708
+ timeoutPolicies.api,
709
+ retryPolicies.standard
710
+ ),
711
+ paymentService: mergePolicies(
712
+ timeoutPolicies.extended,
713
+ retryPolicies.aggressive
714
+ ),
715
+ cacheLayer: servicePolicies.cache,
716
+ };
717
+
718
+ expect(policies.userService.timeout?.ms).toBe(5000);
719
+ expect(policies.paymentService.retry?.attempts).toBe(5);
720
+ expect(policies.cacheLayer.timeout?.ms).toBe(1000);
721
+ });
722
+
723
+ it("DO: Use registry for large apps", () => {
724
+ // Central registry, import everywhere
725
+ const policies = createPolicyRegistry();
726
+ policies.register("external-api", servicePolicies.httpApi);
727
+ policies.register("internal-api", servicePolicies.cache);
728
+
729
+ expect(policies.has("external-api")).toBe(true);
730
+ expect(policies.has("internal-api")).toBe(true);
731
+ });
732
+
733
+ it("DO: Use environment-based policies", () => {
734
+ // Different behavior per environment
735
+ const apiPolicy = envPolicy({
736
+ production: servicePolicies.httpApi,
737
+ development: mergePolicies(timeoutPolicies.fast, retryPolicies.none),
738
+ test: retryPolicies.none,
739
+ });
740
+
741
+ // Test that it works (will use current NODE_ENV or default)
742
+ expect(apiPolicy).toBeDefined();
743
+ });
744
+ });
745
+
746
+ // ============================================================================
747
+ // Export to avoid unused variable warnings
748
+ // ============================================================================
749
+
750
+ export { fetchUser, fetchOrders, fetchData, queryOrders, checkCache };