@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.
- package/README.md +1197 -20
- package/dist/duration.cjs +2 -0
- package/dist/duration.cjs.map +1 -0
- package/dist/duration.d.cts +246 -0
- package/dist/duration.d.ts +246 -0
- package/dist/duration.js +2 -0
- package/dist/duration.js.map +1 -0
- package/dist/index.cjs +5 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/match.cjs +2 -0
- package/dist/match.cjs.map +1 -0
- package/dist/match.d.cts +216 -0
- package/dist/match.d.ts +216 -0
- package/dist/match.js +2 -0
- package/dist/match.js.map +1 -0
- package/dist/schedule.cjs +2 -0
- package/dist/schedule.cjs.map +1 -0
- package/dist/schedule.d.cts +387 -0
- package/dist/schedule.d.ts +387 -0
- package/dist/schedule.js +2 -0
- package/dist/schedule.js.map +1 -0
- package/docs/api.md +30 -0
- package/docs/coming-from-neverthrow.md +103 -10
- package/docs/effect-features-to-port.md +210 -0
- package/docs/match-examples.test.ts +558 -0
- package/docs/match.md +417 -0
- package/docs/policies-examples.test.ts +750 -0
- package/docs/policies.md +508 -0
- package/docs/resource-management-examples.test.ts +729 -0
- package/docs/resource-management.md +509 -0
- package/docs/schedule-examples.test.ts +736 -0
- package/docs/schedule.md +467 -0
- package/docs/tagged-error-examples.test.ts +494 -0
- package/docs/tagged-error.md +730 -0
- package/docs/visualization-examples.test.ts +663 -0
- package/docs/visualization.md +395 -0
- package/docs/visualize-examples.md +1 -1
- 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 };
|