Skip to content

Commit cc1be9d

Browse files
authored
Added removePolicy and listPolicies methods to PolicyPipeline to allow for easier modification and inspection of the pipeline's policies. (#18)
* extend fetch request
1 parent b1f2566 commit cc1be9d

File tree

3 files changed

+222
-22
lines changed

3 files changed

+222
-22
lines changed

src/utils/fetchRequest.ts

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getVersionInfo } from './versionUtils.js';
22

33
export interface FetchPolicy {
4+
id: string;
45
handle(
56
input: string | URL | Request,
67
init: RequestInit,
@@ -16,10 +17,26 @@ export class PolicyPipeline {
1617
this.fetchImpl = fetchImpl ?? fetch;
1718
}
1819

19-
use(policy: FetchPolicy) {
20+
usePolicy(policy: FetchPolicy) {
2021
this.policies.push(policy);
2122
}
2223

24+
removePolicy(policyOrId: FetchPolicy | string) {
25+
if (typeof policyOrId === 'string') {
26+
this.policies = this.policies.filter((p) => p.id !== policyOrId);
27+
} else {
28+
this.policies = this.policies.filter((p) => p !== policyOrId);
29+
}
30+
}
31+
32+
findPolicyById(id: string): FetchPolicy | undefined {
33+
return this.policies.find((p) => p.id === id);
34+
}
35+
36+
listPolicies() {
37+
return this.policies;
38+
}
39+
2340
async fetch(
2441
input: string | URL | Request,
2542
init: RequestInit = {}
@@ -41,7 +58,16 @@ export class PolicyPipeline {
4158
}
4259

4360
export class UserAgentPolicy implements FetchPolicy {
44-
constructor(private userAgent: string) {}
61+
id: string;
62+
63+
constructor(
64+
private userAgent: string,
65+
id?: string
66+
) {
67+
this.id =
68+
id ??
69+
`user-agent-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
70+
}
4571
async handle(
4672
input: string | URL | Request,
4773
init: RequestInit,
@@ -65,24 +91,34 @@ export class UserAgentPolicy implements FetchPolicy {
6591
return next(input, { ...init, headers });
6692
}
6793

68-
static fromVersionInfo(versionInfo: {
69-
name: string;
70-
version: string;
71-
sha: string;
72-
tag: string;
73-
branch: string;
74-
}): UserAgentPolicy {
94+
static fromVersionInfo(
95+
versionInfo: {
96+
name: string;
97+
version: string;
98+
sha: string;
99+
tag: string;
100+
branch: string;
101+
},
102+
id?: string
103+
): UserAgentPolicy {
75104
const userAgent = `${versionInfo.name}/${versionInfo.version} (${versionInfo.branch}, ${versionInfo.tag}, ${versionInfo.sha})`;
76-
return new UserAgentPolicy(userAgent);
105+
return new UserAgentPolicy(userAgent, id);
77106
}
78107
}
79108

80109
export class RetryPolicy implements FetchPolicy {
110+
id: string;
111+
81112
constructor(
82113
private maxRetries: number = 3,
83114
private baseDelayMs: number = 200,
84-
private maxDelayMs: number = 2000
85-
) {}
115+
private maxDelayMs: number = 2000,
116+
id?: string
117+
) {
118+
this.id =
119+
id ??
120+
`retry-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
121+
}
86122

87123
async handle(
88124
input: string | URL | Request,
@@ -119,7 +155,10 @@ export class RetryPolicy implements FetchPolicy {
119155

120156
const pipeline = new PolicyPipeline();
121157
const versionInfo = getVersionInfo();
122-
pipeline.use(UserAgentPolicy.fromVersionInfo(versionInfo));
123-
pipeline.use(new RetryPolicy(3, 200, 2000));
158+
pipeline.usePolicy(
159+
UserAgentPolicy.fromVersionInfo(versionInfo, 'system-user-agent-policy')
160+
);
161+
pipeline.usePolicy(new RetryPolicy(3, 200, 2000, 'system-retry-policy'));
124162

125163
export const fetchClient = pipeline.fetch.bind(pipeline);
164+
export const systemFetchPipeline = pipeline;

test/utils/fetchRequest.test.ts

Lines changed: 168 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,72 @@ function createMockFetch(
2323
}
2424

2525
describe('PolicyPipeline', () => {
26+
describe('usePolicy, removePolicy, and listPolicies', () => {
27+
it('adds policies with usePolicy', () => {
28+
const pipeline = new PolicyPipeline();
29+
const policy1 = new UserAgentPolicy('Agent1');
30+
const policy2 = new RetryPolicy();
31+
32+
pipeline.usePolicy(policy1);
33+
pipeline.usePolicy(policy2);
34+
35+
const policies = pipeline.listPolicies();
36+
expect(policies).toHaveLength(2);
37+
expect(policies[0]).toBe(policy1);
38+
expect(policies[1]).toBe(policy2);
39+
});
40+
41+
it('removes policies with removePolicy', () => {
42+
const pipeline = new PolicyPipeline();
43+
const policy1 = new UserAgentPolicy('Agent1');
44+
const policy2 = new RetryPolicy();
45+
const policy3 = new UserAgentPolicy('Agent3');
46+
47+
pipeline.usePolicy(policy1);
48+
pipeline.usePolicy(policy2);
49+
pipeline.usePolicy(policy3);
50+
51+
pipeline.removePolicy(policy2);
52+
53+
const policies = pipeline.listPolicies();
54+
expect(policies).toHaveLength(2);
55+
expect(policies[0]).toBe(policy1);
56+
expect(policies[1]).toBe(policy3);
57+
});
58+
59+
it('removePolicy does nothing if policy not found', () => {
60+
const pipeline = new PolicyPipeline();
61+
const policy1 = new UserAgentPolicy('Agent1');
62+
const policy2 = new RetryPolicy();
63+
64+
pipeline.usePolicy(policy1);
65+
66+
pipeline.removePolicy(policy2); // Not in the list
67+
68+
const policies = pipeline.listPolicies();
69+
expect(policies).toHaveLength(1);
70+
expect(policies[0]).toBe(policy1);
71+
});
72+
73+
it('listPolicies returns empty array initially', () => {
74+
const pipeline = new PolicyPipeline();
75+
expect(pipeline.listPolicies()).toEqual([]);
76+
});
77+
78+
it('listPolicies returns the policies array', () => {
79+
const pipeline = new PolicyPipeline();
80+
const policy = new UserAgentPolicy('Agent1');
81+
82+
pipeline.usePolicy(policy);
83+
const policies1 = pipeline.listPolicies();
84+
const policies2 = pipeline.listPolicies();
85+
86+
expect(policies1).toBe(policies2); // Same reference
87+
expect(policies1).toEqual(policies2); // Same content
88+
expect(policies1).toContain(policy);
89+
});
90+
});
91+
2692
describe('RetryPolicy', () => {
2793
afterEach(() => {
2894
vi.restoreAllMocks();
@@ -36,7 +102,7 @@ describe('PolicyPipeline', () => {
36102
{ status: 500 }
37103
]);
38104
const pipeline = new PolicyPipeline(mockFetch);
39-
pipeline.use(new RetryPolicy(3, 1, 10)); // Use small delays for test speed
105+
pipeline.usePolicy(new RetryPolicy(3, 1, 10)); // Use small delays for test speed
40106

41107
const response = await pipeline.fetch('http://test', {});
42108

@@ -51,7 +117,7 @@ describe('PolicyPipeline', () => {
51117
{ status: 200, ok: true }
52118
]);
53119
const pipeline = new PolicyPipeline(mockFetch);
54-
pipeline.use(new RetryPolicy(3, 1, 10));
120+
pipeline.usePolicy(new RetryPolicy(3, 1, 10));
55121

56122
const response = await pipeline.fetch('http://test', {});
57123

@@ -63,7 +129,7 @@ describe('PolicyPipeline', () => {
63129
it('does not retry on 400 errors', async () => {
64130
const mockFetch = createMockFetch([{ status: 400 }]);
65131
const pipeline = new PolicyPipeline(mockFetch);
66-
pipeline.use(new RetryPolicy(3, 1, 10));
132+
pipeline.usePolicy(new RetryPolicy(3, 1, 10));
67133

68134
const response = await pipeline.fetch('http://test', {});
69135

@@ -74,7 +140,7 @@ describe('PolicyPipeline', () => {
74140
it('returns immediately on first success', async () => {
75141
const mockFetch = createMockFetch([{ status: 200, ok: true }]);
76142
const pipeline = new PolicyPipeline(mockFetch);
77-
pipeline.use(new RetryPolicy(3, 1, 10));
143+
pipeline.usePolicy(new RetryPolicy(3, 1, 10));
78144

79145
const response = await pipeline.fetch('http://test', {});
80146

@@ -99,7 +165,7 @@ describe('PolicyPipeline', () => {
99165
) as Mock;
100166

101167
const pipeline = new PolicyPipeline(mockFetch as unknown as typeof fetch);
102-
pipeline.use(new UserAgentPolicy('TestAgent/1.0'));
168+
pipeline.usePolicy(new UserAgentPolicy('TestAgent/1.0'));
103169

104170
await pipeline.fetch('http://test', {});
105171

@@ -124,7 +190,7 @@ describe('PolicyPipeline', () => {
124190
) as Mock;
125191

126192
const pipeline = new PolicyPipeline(mockFetch as unknown as typeof fetch);
127-
pipeline.use(new UserAgentPolicy('TestAgent/1.0'));
193+
pipeline.usePolicy(new UserAgentPolicy('TestAgent/1.0'));
128194

129195
await pipeline.fetch('http://test', {
130196
headers: {
@@ -153,12 +219,107 @@ describe('PolicyPipeline', () => {
153219
) as Mock;
154220

155221
const pipeline = new PolicyPipeline(mockFetch as unknown as typeof fetch);
156-
pipeline.use(new UserAgentPolicy('TestAgent/1.0'));
222+
pipeline.usePolicy(new UserAgentPolicy('TestAgent/1.0'));
157223

158224
const headers = new Headers();
159225
await pipeline.fetch('http://test', { headers });
160226

161227
expect(headers.get('User-Agent')).toBe('TestAgent/1.0');
162228
});
163229
});
230+
231+
describe('Policy ID functionality', () => {
232+
it('assigns unique IDs to policies when not provided', () => {
233+
const policy1 = new UserAgentPolicy('Agent1');
234+
const policy2 = new UserAgentPolicy('Agent2');
235+
const policy3 = new RetryPolicy();
236+
237+
expect(policy1.id).toBeDefined();
238+
expect(policy2.id).toBeDefined();
239+
expect(policy3.id).toBeDefined();
240+
expect(policy1.id).not.toBe(policy2.id);
241+
expect(policy2.id).not.toBe(policy3.id);
242+
});
243+
244+
it('uses custom ID when provided', () => {
245+
const customId = 'my-custom-policy';
246+
const policy = new UserAgentPolicy('Agent1', customId);
247+
248+
expect(policy.id).toBe(customId);
249+
});
250+
251+
it('removes policies by ID using removePolicy', () => {
252+
const pipeline = new PolicyPipeline();
253+
const policy1 = new UserAgentPolicy('Agent1', 'policy-1');
254+
const policy2 = new RetryPolicy(3, 200, 2000, 'policy-2');
255+
const policy3 = new UserAgentPolicy('Agent3', 'policy-3');
256+
257+
pipeline.usePolicy(policy1);
258+
pipeline.usePolicy(policy2);
259+
pipeline.usePolicy(policy3);
260+
261+
pipeline.removePolicy('policy-2'); // Remove by ID string
262+
263+
const policies = pipeline.listPolicies();
264+
expect(policies).toHaveLength(2);
265+
expect(policies[0]).toBe(policy1);
266+
expect(policies[1]).toBe(policy3);
267+
});
268+
269+
it('removePolicy supports both policy instance and ID string', () => {
270+
const pipeline = new PolicyPipeline();
271+
const policy1 = new UserAgentPolicy('Agent1', 'policy-1');
272+
const policy2 = new RetryPolicy(3, 200, 2000, 'policy-2');
273+
const policy3 = new UserAgentPolicy('Agent3', 'policy-3');
274+
const policy4 = new UserAgentPolicy('Agent4', 'policy-4');
275+
276+
pipeline.usePolicy(policy1);
277+
pipeline.usePolicy(policy2);
278+
pipeline.usePolicy(policy3);
279+
pipeline.usePolicy(policy4);
280+
281+
// Remove by policy instance
282+
pipeline.removePolicy(policy2);
283+
284+
// Remove by ID string
285+
pipeline.removePolicy('policy-4');
286+
287+
const policies = pipeline.listPolicies();
288+
expect(policies).toHaveLength(2);
289+
expect(policies[0]).toBe(policy1);
290+
expect(policies[1]).toBe(policy3);
291+
});
292+
293+
it('finds policies by ID', () => {
294+
const pipeline = new PolicyPipeline();
295+
const policy1 = new UserAgentPolicy('Agent1', 'policy-1');
296+
const policy2 = new RetryPolicy(3, 200, 2000, 'policy-2');
297+
298+
pipeline.usePolicy(policy1);
299+
pipeline.usePolicy(policy2);
300+
301+
expect(pipeline.findPolicyById('policy-1')).toBe(policy1);
302+
expect(pipeline.findPolicyById('policy-2')).toBe(policy2);
303+
expect(pipeline.findPolicyById('non-existent')).toBeUndefined();
304+
});
305+
306+
it('fromVersionInfo accepts optional ID parameter', () => {
307+
const versionInfo = {
308+
name: 'test-app',
309+
version: '1.0.0',
310+
sha: 'abc123',
311+
tag: 'v1.0.0',
312+
branch: 'main'
313+
};
314+
315+
const policyWithoutId = UserAgentPolicy.fromVersionInfo(versionInfo);
316+
const policyWithId = UserAgentPolicy.fromVersionInfo(
317+
versionInfo,
318+
'custom-id'
319+
);
320+
321+
expect(policyWithoutId.id).toBeDefined();
322+
expect(policyWithId.id).toBe('custom-id');
323+
});
324+
});
164325
});

test/utils/fetchRequestUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function setupFetch(overrides?: any) {
1919
// Build a real pipeline with UserAgentPolicy
2020
const userAgent = 'TestServer/1.0.0 (default, no-tag, abcdef)';
2121
const pipeline = new PolicyPipeline(mockFetch);
22-
pipeline.use(new UserAgentPolicy(userAgent));
22+
pipeline.usePolicy(new UserAgentPolicy(userAgent));
2323

2424
return { fetch: pipeline.fetch.bind(pipeline), mockFetch };
2525
}

0 commit comments

Comments
 (0)