Skip to content

Commit 7250d32

Browse files
refactor: define typed API for client call context (#232)
# Description Provides typed API to read and update context to make predefined and externally defined keys composable in the same way. ```ts export type SystemParameters = Record<string, string>; export const systemParametersKey = new ClientCallContextKey<SystemParameters>('systemParameters'); const myContext = ClientCallContext.create( systemParametersKey.set({ 'X-A2A-Extensions': 'http://example.com' }) ); const myParameters = systemParametersKey.get(myContext); ``` --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 101b95c commit 7250d32

File tree

5 files changed

+96
-4
lines changed

5 files changed

+96
-4
lines changed

src/client/context.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Opaque context object to carry per-call context data.
3+
* Use {@link ClientCallContextKey} to create typed keys for storing and retrieving values.
4+
*/
5+
export type ClientCallContext = Record<symbol, unknown>;
6+
7+
/**
8+
* Function that applies an update to a {@link ClientCallContext}.
9+
*/
10+
export type ContextUpdate = (context: ClientCallContext) => void;
11+
12+
export const ClientCallContext = {
13+
/**
14+
* Create a new {@link ClientCallContext} with optional updates applied.
15+
*/
16+
create: (...updates: ContextUpdate[]): ClientCallContext => {
17+
return ClientCallContext.createFrom(undefined, ...updates);
18+
},
19+
20+
/**
21+
* Create a new {@link ClientCallContext} based on an existing one with updates applied.
22+
*/
23+
createFrom: (
24+
context: ClientCallContext | undefined,
25+
...updates: ContextUpdate[]
26+
): ClientCallContext => {
27+
const result = context ? { ...context } : {};
28+
for (const update of updates) {
29+
update(result);
30+
}
31+
return result;
32+
},
33+
};
34+
35+
/**
36+
* Each instance represents a unique key for storing
37+
* and retrieving typed values in a {@link ClientCallContext}.
38+
*
39+
* @example
40+
* ```ts
41+
* const key = new ClientCallContextKey<string>('My key');
42+
* const context = ClientCallContext.create(key.set('example-value'));
43+
* const value = key.get(context); // 'example-value'
44+
* ```
45+
*/
46+
export class ClientCallContextKey<T> {
47+
public readonly symbol: symbol;
48+
49+
constructor(description: string) {
50+
this.symbol = Symbol(description);
51+
}
52+
53+
set(value: T): ContextUpdate {
54+
return (context) => {
55+
context[this.symbol] = value;
56+
};
57+
}
58+
59+
get(context: ClientCallContext): T | undefined {
60+
return context[this.symbol] as T | undefined;
61+
}
62+
}

src/client/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ export {
2525
ClientCallInput,
2626
ClientCallResult,
2727
} from './interceptors.js';
28+
export { ClientCallContext, ContextUpdate, ClientCallContextKey } from './context.js';

src/client/multitransport-client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
AgentCard,
1212
} from '../types.js';
1313
import { A2AStreamEventData, SendMessageResult } from './client.js';
14+
import { ClientCallContext } from './context.js';
1415
import {
1516
CallInterceptor,
1617
BeforeArgs,
@@ -55,7 +56,7 @@ export interface RequestOptions {
5556
/**
5657
* Arbitrary data available to interceptors and transport implementation.
5758
*/
58-
context?: Map<string, unknown>;
59+
context?: ClientCallContext;
5960
}
6061

6162
export class Client {

test/client/context.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { expect } from 'chai';
2+
import { ClientCallContext, ClientCallContextKey } from '../../src/client/context.js';
3+
4+
describe('ClientCallContext', () => {
5+
const testKey = new ClientCallContextKey<string>('My key');
6+
7+
it('should create new context', () => {
8+
const context = ClientCallContext.create(testKey.set('example-value'));
9+
expect(testKey.get(context)).to.be.equal('example-value');
10+
});
11+
12+
it('should create context from existing', () => {
13+
const existingContext = ClientCallContext.createFrom(undefined, testKey.set('example-value'));
14+
const context = ClientCallContext.createFrom(existingContext, testKey.set('new-value'));
15+
expect(testKey.get(context)).to.be.equal('new-value');
16+
});
17+
});
18+
19+
describe('ClientCallContextKey', () => {
20+
it('should be unique', () => {
21+
const key1 = new ClientCallContextKey<string>('My key');
22+
const key2 = new ClientCallContextKey<string>('My key');
23+
expect(key1.symbol).to.not.equal(key2.symbol);
24+
const context = ClientCallContext.create(key1.set('key1-value'), key2.set('key2-value'));
25+
expect(key1.get(context)).to.be.equal('key1-value');
26+
expect(key2.get(context)).to.be.equal('key2-value');
27+
});
28+
});

test/client/multitransport-client.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ describe('Client', () => {
438438
interceptors: [
439439
{
440440
before: async (args) => {
441-
args.options = { context: new Map([['foo', 'bar']]) };
441+
args.options = { context: { [Symbol.for('foo')]: 'bar' } };
442442
},
443443
after: async () => {},
444444
},
@@ -456,8 +456,8 @@ describe('Client', () => {
456456

457457
const result = await client.getTask(params);
458458

459-
expect(transport.getTask.calledOnceWith(params, { context: new Map([['foo', 'bar']]) })).to.be
460-
.true;
459+
expect(transport.getTask.calledOnceWith(params, { context: { [Symbol.for('foo')]: 'bar' } }))
460+
.to.be.true;
461461
expect(result).to.equal(task);
462462
});
463463

0 commit comments

Comments
 (0)