Skip to content

Commit ca9aabf

Browse files
committed
Extend runtime-context to support key-based multi-context storage and retrieval
1 parent 2352b92 commit ca9aabf

3 files changed

Lines changed: 178 additions & 18 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@oamm/runtime-context",
3-
"version": "1.0.1",
3+
"version": "1.0.2",
44
"description": "Shared, injectable request-scoped runtime context for Node.js and Edge runtimes",
55
"type": "module",
66
"main": "./dist/index.js",

src/index.ts

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,29 +39,71 @@ function getStorage<TContext>(): ContextStorage<TContext> {
3939
return storage!;
4040
}
4141

42-
export function getContext<TContext = DefaultContext>(): TContext | undefined {
43-
return getStorage<TContext>().getStore();
42+
export function getContext<TContext = DefaultContext>(key?: any): TContext | undefined {
43+
const store = getStorage<any>().getStore();
44+
if (key !== undefined) {
45+
if (store instanceof Map) {
46+
return store.get(key);
47+
}
48+
if (typeof store === 'object' && store !== null) {
49+
return (store as any)[key];
50+
}
51+
return undefined;
52+
}
53+
return store as TContext | undefined;
4454
}
4555

46-
export function requireContext<TContext = DefaultContext>(): TContext {
47-
const ctx = getContext<TContext>();
56+
export function requireContext<TContext = DefaultContext>(key?: any): TContext {
57+
const ctx = getContext<TContext>(key);
4858
if (ctx === undefined) {
49-
throw new Error('Runtime context is missing. Ensure you are running within runWithContext.');
59+
throw new Error(
60+
`Runtime context${key ? ` for ${String(key)}` : ''} is missing. Ensure you are running within runWithContext.`
61+
);
5062
}
5163
return ctx;
5264
}
5365

5466
export function runWithContext<TContext, R>(
5567
ctx: TContext,
5668
fn: () => R
57-
): R {
58-
return getStorage<TContext>().run(ctx, fn);
69+
): R;
70+
export function runWithContext<TContext, R>(
71+
key: any,
72+
ctx: TContext,
73+
fn: () => R
74+
): R;
75+
export function runWithContext<TContext, R>(...args: any[]): R {
76+
const storage = getStorage();
77+
if (args.length === 3) {
78+
const [key, ctx, fn] = args;
79+
const parent = storage.getStore();
80+
const map = parent instanceof Map ? new Map(parent) : new Map();
81+
map.set(key, ctx);
82+
return storage.run(map, fn);
83+
}
84+
const [ctx, fn] = args;
85+
return storage.run(ctx, fn);
5986
}
6087

6188
export function ensureContext<TContext, R>(
6289
create: () => TContext,
6390
fn: () => Promise<R> | R
64-
): Promise<R> | R {
91+
): Promise<R> | R;
92+
export function ensureContext<TContext, R>(
93+
key: any,
94+
create: () => TContext,
95+
fn: () => Promise<R> | R
96+
): Promise<R> | R;
97+
export function ensureContext<TContext, R>(...args: any[]): Promise<R> | R {
98+
if (args.length === 3) {
99+
const [key, create, fn] = args;
100+
const existing = getContext<TContext>(key);
101+
if (existing !== undefined) {
102+
return fn();
103+
}
104+
return runWithContext(key, create(), fn);
105+
}
106+
const [create, fn] = args;
65107
const existing = getContext<TContext>();
66108
if (existing !== undefined) {
67109
return fn();
@@ -73,30 +115,48 @@ export function ensureContext<TContext, R>(
73115

74116
export function setValue<TContext extends object = DefaultContext, K extends keyof TContext = keyof TContext>(
75117
key: K,
76-
value: TContext[K]
118+
value: TContext[K],
119+
contextKey?: any
77120
): void {
78-
const ctx = requireContext<TContext>();
121+
const ctx = requireContext<any>(contextKey);
122+
if (ctx instanceof Map) {
123+
ctx.set(key, value);
124+
return;
125+
}
79126
if (typeof ctx !== 'object' || ctx === null) {
80-
throw new Error('Context must be an object to use setValue.');
127+
throw new Error('Context must be an object or Map to use setValue.');
81128
}
82129
(ctx as any)[key] = value;
83130
}
84131

85132
export function getValue<TContext extends object = DefaultContext, K extends keyof TContext = keyof TContext>(
86-
key: K
133+
key: K,
134+
contextKey?: any
87135
): TContext[K] | undefined {
88-
const ctx = getContext<TContext>();
136+
const ctx = getContext<any>(contextKey);
89137
if (!ctx) return undefined;
138+
if (ctx instanceof Map) {
139+
return ctx.get(key);
140+
}
90141
if (typeof ctx !== 'object') {
91-
throw new Error('Context must be an object to use getValue.');
142+
throw new Error('Context must be an object or Map to use getValue.');
92143
}
93144
return (ctx as any)[key];
94145
}
95146

96-
export function mergeContext<TContext extends object = DefaultContext>(partial: Partial<TContext>): void {
97-
const ctx = requireContext<TContext>();
147+
export function mergeContext<TContext extends object = DefaultContext>(
148+
partial: Partial<TContext>,
149+
contextKey?: any
150+
): void {
151+
const ctx = requireContext<any>(contextKey);
152+
if (ctx instanceof Map) {
153+
for (const [key, value] of Object.entries(partial)) {
154+
ctx.set(key, value);
155+
}
156+
return;
157+
}
98158
if (typeof ctx !== 'object' || ctx === null) {
99-
throw new Error('Context must be an object to use mergeContext.');
159+
throw new Error('Context must be an object or Map to use mergeContext.');
100160
}
101161
Object.assign(ctx, partial);
102162
}

tests/context.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,104 @@ describe('Runtime Context', () => {
120120
expect(() => runWithContext({}, () => {})).toThrow(/Runtime context storage is missing/);
121121
vi.restoreAllMocks();
122122
});
123+
124+
describe('Map-based Multi-Context', () => {
125+
it('should support multiple contexts using keys', () => {
126+
const tokenKitData = { token: 'abc' };
127+
const sessionData = { user: 'admin' };
128+
129+
runWithContext('token-kit', tokenKitData, () => {
130+
expect(getContext('token-kit')).toBe(tokenKitData);
131+
expect(requireContext('token-kit')).toBe(tokenKitData);
132+
133+
runWithContext('session', sessionData, () => {
134+
expect(getContext('token-kit')).toBe(tokenKitData);
135+
expect(getContext('session')).toBe(sessionData);
136+
137+
// Without key, it should return the Map (the whole store)
138+
const store = getContext();
139+
expect(store).toBeInstanceOf(Map);
140+
expect((store as any as Map<any, any>).get('token-kit')).toBe(tokenKitData);
141+
expect((store as any as Map<any, any>).get('session')).toBe(sessionData);
142+
});
143+
144+
expect(getContext('session')).toBeUndefined();
145+
expect(getContext('token-kit')).toBe(tokenKitData);
146+
});
147+
});
148+
149+
it('should support classes as keys', () => {
150+
class TokenKit {
151+
constructor(public token: string) {}
152+
}
153+
class Session {
154+
constructor(public user: string) {}
155+
}
156+
157+
const tk = new TokenKit('abc');
158+
const s = new Session('admin');
159+
160+
runWithContext(TokenKit, tk, () => {
161+
runWithContext(Session, s, () => {
162+
expect(getContext(TokenKit)).toBe(tk);
163+
expect(getContext(Session)).toBe(s);
164+
});
165+
});
166+
});
167+
168+
it('should support ensureContext with keys', async () => {
169+
const key = Symbol('key');
170+
const data1 = { a: 1 };
171+
const data2 = { a: 2 };
172+
173+
const result = await ensureContext(
174+
key,
175+
() => data1,
176+
async () => {
177+
expect(getContext(key)).toBe(data1);
178+
179+
return await ensureContext(key, () => data2, () => {
180+
expect(getContext(key)).toBe(data1); // Should reuse existing
181+
return 'done';
182+
});
183+
}
184+
);
185+
expect(result).toBe('done');
186+
});
187+
188+
it('should support setValue/getValue/mergeContext with context keys', () => {
189+
const key = 'my-context';
190+
const data = { a: 1 } as any;
191+
192+
runWithContext(key, data, () => {
193+
setValue('b', 2, key);
194+
expect(getValue('b', key)).toBe(2);
195+
expect(data.b).toBe(2);
196+
197+
mergeContext({ c: 3 }, key);
198+
expect(data.c).toBe(3);
199+
});
200+
});
201+
202+
it('should support setValue/getValue/mergeContext on Map context', () => {
203+
const key = 'my-map';
204+
const data = new Map();
205+
206+
runWithContext(key, data, () => {
207+
setValue('foo', 'bar', key);
208+
expect(getValue('foo', key)).toBe('bar');
209+
expect(data.get('foo')).toBe('bar');
210+
211+
mergeContext({ hello: 'world' }, key);
212+
expect(data.get('hello')).toBe('world');
213+
});
214+
});
215+
216+
it('should fall back to object property if not a Map', () => {
217+
const ctx = { foo: 'bar' };
218+
runWithContext(ctx, () => {
219+
expect(getContext('foo')).toBe('bar');
220+
});
221+
});
222+
});
123223
});

0 commit comments

Comments
 (0)