Skip to content

Commit e60324f

Browse files
committed
feat: add caching for the context based client
1 parent bde5bf7 commit e60324f

8 files changed

Lines changed: 547 additions & 52 deletions

File tree

packages/react-client/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ export {
3030
type OperationSchema,
3131
type RequestFnPayload,
3232
} from './lib/requestFn.js';
33+
export { memoizeFunctionCall } from './lib/memoizeFunctionCall.js';
Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import { memoizeFunctionCall } from './memoizeFunctionCall.js';
3+
4+
describe('memoizeFunctionCall', () => {
5+
it('should create and return new value on first call', () => {
6+
const memoCache = new WeakMap();
7+
const factory = vi.fn((arg: { id: number }) => ({ result: arg.id * 2 }));
8+
const arg = { id: 5 };
9+
10+
const result = memoizeFunctionCall(memoCache, factory, arg);
11+
12+
expect(result).toEqual({ result: 10 });
13+
expect(factory).toHaveBeenCalledTimes(1);
14+
expect(factory).toHaveBeenCalledWith(arg);
15+
});
16+
17+
it('should return cached value on second call with same arguments', () => {
18+
const memoCache = new WeakMap();
19+
const factory = vi.fn((arg: { id: number }) => ({ result: arg.id * 2 }));
20+
const arg = { id: 5 };
21+
22+
const first = memoizeFunctionCall(memoCache, factory, arg);
23+
const second = memoizeFunctionCall(memoCache, factory, arg);
24+
25+
expect(first).toBe(second);
26+
expect(factory).toHaveBeenCalledTimes(1);
27+
});
28+
29+
it('should create new value for different arguments', () => {
30+
const memoCache = new WeakMap();
31+
const factory = vi.fn((arg: { id: number }) => ({ result: arg.id * 2 }));
32+
const arg1 = { id: 5 };
33+
const arg2 = { id: 10 };
34+
35+
const result1 = memoizeFunctionCall(memoCache, factory, arg1);
36+
const result2 = memoizeFunctionCall(memoCache, factory, arg2);
37+
38+
expect(result1).toEqual({ result: 10 });
39+
expect(result2).toEqual({ result: 20 });
40+
expect(factory).toHaveBeenCalledTimes(2);
41+
});
42+
43+
it('should use referential identity, not value equality', () => {
44+
const memoCache = new WeakMap();
45+
const factory = vi.fn((arg: { id: number }) => ({ result: arg.id * 2 }));
46+
const arg1 = { id: 5 };
47+
const arg2 = { id: 5 };
48+
49+
const result1 = memoizeFunctionCall(memoCache, factory, arg1);
50+
const result2 = memoizeFunctionCall(memoCache, factory, arg2);
51+
52+
expect(result1).not.toBe(result2);
53+
expect(factory).toHaveBeenCalledTimes(2);
54+
});
55+
56+
it('should handle multiple arguments', () => {
57+
const memoCache = new WeakMap();
58+
const factory = vi.fn((arg1: { a: number }, arg2: { b: number }) => ({
59+
sum: arg1.a + arg2.b,
60+
}));
61+
const arg1 = { a: 1 };
62+
const arg2 = { b: 2 };
63+
64+
const first = memoizeFunctionCall(memoCache, factory, arg1, arg2);
65+
const second = memoizeFunctionCall(memoCache, factory, arg1, arg2);
66+
67+
expect(first).toBe(second);
68+
expect(first).toEqual({ sum: 3 });
69+
expect(factory).toHaveBeenCalledTimes(1);
70+
});
71+
72+
it('should create different values for different argument combinations', () => {
73+
const memoCache = new WeakMap();
74+
const factory = vi.fn((arg1: { a: number }, arg2: { b: number }) => ({
75+
sum: arg1.a + arg2.b,
76+
}));
77+
const arg1a = { a: 1 };
78+
const arg1b = { a: 1 };
79+
const arg2a = { b: 2 };
80+
const arg2b = { b: 3 };
81+
82+
const result1 = memoizeFunctionCall(memoCache, factory, arg1a, arg2a);
83+
const result2 = memoizeFunctionCall(memoCache, factory, arg1a, arg2b);
84+
const result3 = memoizeFunctionCall(memoCache, factory, arg1b, arg2a);
85+
86+
expect(result1).toEqual({ sum: 3 });
87+
expect(result2).toEqual({ sum: 4 });
88+
expect(result3).toEqual({ sum: 3 });
89+
expect(result1).not.toBe(result3);
90+
expect(factory).toHaveBeenCalledTimes(3);
91+
});
92+
93+
it('should handle three or more arguments', () => {
94+
const memoCache = new WeakMap();
95+
const factory = vi.fn(
96+
(arg1: { a: number }, arg2: { b: number }, arg3: { c: number }) => ({
97+
sum: arg1.a + arg2.b + arg3.c,
98+
})
99+
);
100+
const arg1 = { a: 1 };
101+
const arg2 = { b: 2 };
102+
const arg3 = { c: 3 };
103+
104+
const first = memoizeFunctionCall(memoCache, factory, arg1, arg2, arg3);
105+
const second = memoizeFunctionCall(memoCache, factory, arg1, arg2, arg3);
106+
107+
expect(first).toBe(second);
108+
expect(first).toEqual({ sum: 6 });
109+
expect(factory).toHaveBeenCalledTimes(1);
110+
});
111+
112+
it('should maintain separate caches for different factories', () => {
113+
const memoCache = new WeakMap();
114+
const factory1 = vi.fn((arg: { id: number }) => ({
115+
type: 'a',
116+
id: arg.id,
117+
}));
118+
const factory2 = vi.fn((arg: { id: number }) => ({
119+
type: 'b',
120+
id: arg.id,
121+
}));
122+
const arg = { id: 5 };
123+
124+
const result1 = memoizeFunctionCall(memoCache, factory1, arg);
125+
const result2 = memoizeFunctionCall(memoCache, factory2, arg);
126+
127+
expect(result1).toEqual({ type: 'a', id: 5 });
128+
expect(result2).toEqual({ type: 'b', id: 5 });
129+
expect(result1).not.toBe(result2);
130+
expect(factory1).toHaveBeenCalledTimes(1);
131+
expect(factory2).toHaveBeenCalledTimes(1);
132+
});
133+
134+
it('should cache undefined values correctly', () => {
135+
const memoCache = new WeakMap();
136+
const factory = vi.fn((_arg: { id: number }) => undefined);
137+
const arg = { id: 5 };
138+
139+
const first = memoizeFunctionCall(memoCache, factory, arg);
140+
const second = memoizeFunctionCall(memoCache, factory, arg);
141+
142+
expect(first).toBeUndefined();
143+
expect(second).toBeUndefined();
144+
expect(first).toBe(second);
145+
expect(factory).toHaveBeenCalledTimes(1);
146+
});
147+
148+
it('should cache null values correctly', () => {
149+
const memoCache = new WeakMap();
150+
const factory = vi.fn((_arg: { id: number }) => null);
151+
const arg = { id: 5 };
152+
153+
const first = memoizeFunctionCall(memoCache, factory, arg);
154+
const second = memoizeFunctionCall(memoCache, factory, arg);
155+
156+
expect(first).toBeNull();
157+
expect(second).toBeNull();
158+
expect(first).toBe(second);
159+
expect(factory).toHaveBeenCalledTimes(1);
160+
});
161+
162+
it('should cache primitive return values', () => {
163+
const memoCache = new WeakMap();
164+
const factory = vi.fn((arg: { id: number }) => arg.id * 2);
165+
const arg = { id: 5 };
166+
167+
const first = memoizeFunctionCall(memoCache, factory, arg);
168+
const second = memoizeFunctionCall(memoCache, factory, arg);
169+
170+
expect(first).toBe(10);
171+
expect(second).toBe(10);
172+
expect(first).toBe(second);
173+
expect(factory).toHaveBeenCalledTimes(1);
174+
});
175+
176+
it('should cache function return values', () => {
177+
const memoCache = new WeakMap();
178+
const factory = vi.fn((arg: { id: number }) => () => arg.id);
179+
const arg = { id: 5 };
180+
181+
const first = memoizeFunctionCall(memoCache, factory, arg) as () => number;
182+
const second = memoizeFunctionCall(memoCache, factory, arg) as () => number;
183+
184+
expect(first).toBe(second);
185+
expect(first()).toBe(5);
186+
expect(second()).toBe(5);
187+
expect(factory).toHaveBeenCalledTimes(1);
188+
});
189+
190+
it('should handle complex nested argument structures', () => {
191+
const memoCache = new WeakMap();
192+
const factory = vi.fn(
193+
(
194+
config: { name: string },
195+
options: { enabled: boolean },
196+
metadata: { tags: string[] }
197+
) => ({
198+
config: config.name,
199+
enabled: options.enabled,
200+
tags: metadata.tags,
201+
})
202+
);
203+
const config = { name: 'test' };
204+
const options = { enabled: true };
205+
const metadata = { tags: ['a', 'b'] };
206+
207+
const first = memoizeFunctionCall(
208+
memoCache,
209+
factory,
210+
config,
211+
options,
212+
metadata
213+
);
214+
const second = memoizeFunctionCall(
215+
memoCache,
216+
factory,
217+
config,
218+
options,
219+
metadata
220+
);
221+
222+
expect(first).toBe(second);
223+
expect(first).toEqual({
224+
config: 'test',
225+
enabled: true,
226+
tags: ['a', 'b'],
227+
});
228+
expect(factory).toHaveBeenCalledTimes(1);
229+
});
230+
231+
it('should create new cache entry when one argument changes', () => {
232+
const memoCache = new WeakMap();
233+
const factory = vi.fn((arg1: { a: number }, arg2: { b: number }) => ({
234+
sum: arg1.a + arg2.b,
235+
}));
236+
const arg1 = { a: 1 };
237+
const arg2a = { b: 2 };
238+
const arg2b = { b: 2 };
239+
240+
const result1 = memoizeFunctionCall(memoCache, factory, arg1, arg2a);
241+
const result2 = memoizeFunctionCall(memoCache, factory, arg1, arg2b);
242+
243+
expect(result1).not.toBe(result2);
244+
expect(factory).toHaveBeenCalledTimes(2);
245+
});
246+
247+
it('should throw error when undefined is passed as argument', () => {
248+
const memoCache = new WeakMap();
249+
const factory = vi.fn((arg: { id: number }) => ({ result: arg.id }));
250+
251+
expect(() => {
252+
memoizeFunctionCall(memoCache, factory, undefined as any);
253+
}).toThrow();
254+
});
255+
256+
it('should throw error when null is passed as argument', () => {
257+
const memoCache = new WeakMap();
258+
const factory = vi.fn((arg: { id: number }) => ({ result: arg.id }));
259+
260+
expect(() => {
261+
memoizeFunctionCall(memoCache, factory, null as any);
262+
}).toThrow();
263+
});
264+
265+
it('should throw error when undefined is passed as one of multiple arguments', () => {
266+
const memoCache = new WeakMap();
267+
const factory = vi.fn((arg1: { a: number }, arg2: { b: number }) => ({
268+
sum: arg1.a + arg2.b,
269+
}));
270+
const arg1 = { a: 1 };
271+
272+
expect(() => {
273+
memoizeFunctionCall(memoCache, factory, arg1, undefined as any);
274+
}).toThrow();
275+
});
276+
277+
it('should throw error when null is passed as one of multiple arguments', () => {
278+
const memoCache = new WeakMap();
279+
const factory = vi.fn((arg1: { a: number }, arg2: { b: number }) => ({
280+
sum: arg1.a + arg2.b,
281+
}));
282+
const arg1 = { a: 1 };
283+
284+
expect(() => {
285+
memoizeFunctionCall(memoCache, factory, arg1, null as any);
286+
}).toThrow();
287+
});
288+
289+
it('should throw error when undefined is passed as first argument in multiple arguments', () => {
290+
const memoCache = new WeakMap();
291+
const factory = vi.fn((arg1: { a: number }, arg2: { b: number }) => ({
292+
sum: arg1.a + arg2.b,
293+
}));
294+
const arg2 = { b: 2 };
295+
296+
expect(() => {
297+
memoizeFunctionCall(memoCache, factory, undefined as any, arg2);
298+
}).toThrow();
299+
});
300+
301+
it('should throw error when null is passed as first argument in multiple arguments', () => {
302+
const memoCache = new WeakMap();
303+
const factory = vi.fn((arg1: { a: number }, arg2: { b: number }) => ({
304+
sum: arg1.a + arg2.b,
305+
}));
306+
const arg2 = { b: 2 };
307+
308+
expect(() => {
309+
memoizeFunctionCall(memoCache, factory, null as any, arg2);
310+
}).toThrow();
311+
});
312+
313+
it('should cache correctly when calling same factory with 2 args then 3 args with same first two', () => {
314+
const memoCache = new WeakMap();
315+
const factory = vi.fn(
316+
(arg1: { a: number }, arg2: { b: number }, arg3?: { c: number }) => ({
317+
sum: arg1.a + arg2.b + (arg3?.c ?? 0),
318+
})
319+
) as (
320+
arg1: { a: number },
321+
arg2: { b: number },
322+
arg3?: { c: number }
323+
) => { sum: number };
324+
const arg1 = { a: 1 };
325+
const arg2 = { b: 2 };
326+
const arg3 = { c: 3 };
327+
328+
const first2Args = memoizeFunctionCall(memoCache, factory, arg1, arg2);
329+
const second2Args = memoizeFunctionCall(memoCache, factory, arg1, arg2);
330+
const first3Args = memoizeFunctionCall(
331+
memoCache,
332+
factory,
333+
arg1,
334+
arg2,
335+
arg3
336+
);
337+
const second3Args = memoizeFunctionCall(
338+
memoCache,
339+
factory,
340+
arg1,
341+
arg2,
342+
arg3
343+
);
344+
345+
expect(first2Args).toBe(second2Args);
346+
expect(first3Args).toBe(second3Args);
347+
expect(first2Args).not.toBe(first3Args);
348+
expect(factory).toHaveBeenCalledTimes(2);
349+
});
350+
351+
it('should cache correctly when calling same factory with 3 args then 2 args with same first two', () => {
352+
const memoCache = new WeakMap();
353+
const factory = vi.fn(
354+
(arg1: { a: number }, arg2: { b: number }, arg3?: { c: number }) => ({
355+
sum: arg1.a + arg2.b + (arg3?.c ?? 0),
356+
})
357+
) as (
358+
arg1: { a: number },
359+
arg2: { b: number },
360+
arg3?: { c: number }
361+
) => { sum: number };
362+
const arg1 = { a: 1 };
363+
const arg2 = { b: 2 };
364+
const arg3 = { c: 3 };
365+
366+
const first3Args = memoizeFunctionCall(
367+
memoCache,
368+
factory,
369+
arg1,
370+
arg2,
371+
arg3
372+
);
373+
const second3Args = memoizeFunctionCall(
374+
memoCache,
375+
factory,
376+
arg1,
377+
arg2,
378+
arg3
379+
);
380+
const first2Args = memoizeFunctionCall(memoCache, factory, arg1, arg2);
381+
const second2Args = memoizeFunctionCall(memoCache, factory, arg1, arg2);
382+
383+
expect(first3Args).toBe(second3Args);
384+
expect(first2Args).toBe(second2Args);
385+
expect(first3Args).not.toBe(first2Args);
386+
expect(factory).toHaveBeenCalledTimes(2);
387+
});
388+
});

0 commit comments

Comments
 (0)