Skip to content

Commit c7b0386

Browse files
authored
fix: added support for forceRefresh cache in invokeAPI (#992)
1 parent 3d420aa commit c7b0386

File tree

8 files changed

+132
-7
lines changed

8 files changed

+132
-7
lines changed

.changeset/tidy-pants-kiss.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@ensembleui/react-framework": patch
3+
"@ensembleui/react-kitchen-sink": patch
4+
"@ensembleui/react-runtime": patch
5+
---
6+
7+
Added support for bypassCache cache in invokeAPI

apps/kitchen-sink/src/ensemble/screens/home.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -504,8 +504,7 @@ API:
504504

505505
getDummyProducts:
506506
method: GET
507-
cache: true
508-
cacheTime: 10
507+
cacheExpirySeconds: 10
509508
uri: https://randomuser.me/api/?results=1
510509

511510
getDummyNumbers:

packages/framework/src/api/data.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
EnsembleActionHookResult,
77
EnsembleMockResponse,
88
EnsembleSocketModel,
9+
InvokeAPIOptions,
910
} from "../shared";
1011
import { screenDataAtom, type ScreenContextDefinition } from "../state";
1112
import { isUsingMockResponse } from "../appConfig";
@@ -18,6 +19,7 @@ export const invokeAPI = async (
1819
context?: { [key: string]: unknown },
1920
evaluatedMockResponse?: string | EnsembleMockResponse,
2021
setter?: Setter,
22+
options?: InvokeAPIOptions,
2123
): Promise<Response | undefined> => {
2224
const api = screenContext.model?.apis?.find(
2325
(model) => model.name === apiName,
@@ -44,7 +46,7 @@ export const invokeAPI = async (
4446
setter(screenDataAtom, update);
4547
}
4648

47-
// If mock resposne does not exist, fetch the data directly from the API
49+
// If mock response does not exist, fetch the data directly from the API
4850
const useMockResponse =
4951
has(api, "mockResponse") && isUsingMockResponse(screenContext.app?.id);
5052

@@ -62,7 +64,10 @@ export const invokeAPI = async (
6264
useMockResponse,
6365
},
6466
),
65-
staleTime: api.cacheExpirySeconds ? api.cacheExpirySeconds * 1000 : 0,
67+
staleTime:
68+
api.cacheExpirySeconds && !options?.bypassCache
69+
? api.cacheExpirySeconds * 1000
70+
: 0,
6671
});
6772

6873
if (setter) {

packages/framework/src/hooks/useCommandCallback.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import type {
2323
EnsembleScreenModel,
2424
EnsembleWidget,
25+
InvokeAPIOptions,
2526
NavigateExternalScreen,
2627
NavigateModalScreenAction,
2728
NavigateScreenAction,
@@ -98,6 +99,7 @@ export const useCommandCallback = <
9899
invokeAPI: async (
99100
apiName: string,
100101
apiInputs?: { [key: string]: unknown },
102+
options?: InvokeAPIOptions,
101103
) =>
102104
invokeAPI(
103105
apiName,
@@ -113,6 +115,7 @@ export const useCommandCallback = <
113115
},
114116
undefined,
115117
set,
118+
options,
116119
),
117120
navigateExternalScreen: (url: NavigateExternalScreen) =>
118121
navigateExternalScreen(url),

packages/framework/src/shared/actions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface InvokeAPIAction {
2525
name: string;
2626
/** Specify the key/value pairs to pass into the API */
2727
inputs?: { [key: string]: Expression<unknown> };
28+
/** Forcefully clears the cache for this API invocation */
29+
bypassCache?: boolean;
2830
/** execute an Action upon successful completion of the API */
2931
onResponse?: EnsembleAction;
3032
/** execute an Action upon error */
@@ -219,3 +221,7 @@ export type EnsembleAction =
219221
| { messageSocket?: SendSocketMessageAction }
220222
| { disconnectSocket?: DisconnectSocketAction }
221223
| { dispatchEvent?: DispatchEventAction };
224+
225+
export interface InvokeAPIOptions {
226+
bypassCache?: boolean;
227+
}

packages/runtime/src/runtime/hooks/__tests__/useExecuteCode.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => (
3434
uri: "https://dummyjson.com/products?skip=${skip}&limit=${limit}",
3535
inputs: ["skip", "limit"],
3636
},
37+
{
38+
name: "getDummyUser",
39+
method: "GET",
40+
uri: "https://randomuser.me/api/?results=1",
41+
cacheExpirySeconds: 120,
42+
},
3743
],
3844
}}
3945
>
@@ -111,6 +117,37 @@ test("call ensemble.invokeAPI", async () => {
111117
expect(execResult).toBe(apiConfig.limit);
112118
});
113119

120+
test("call ensemble.invokeAPI with bypassCache", async () => {
121+
const { result: withoutForce } = renderHook(
122+
() =>
123+
useExecuteCode(
124+
"ensemble.invokeAPI('getDummyUser', null).then((res) => res.body.results[0].email)",
125+
),
126+
{ wrapper },
127+
);
128+
129+
const { result: withForce } = renderHook(
130+
() =>
131+
useExecuteCode(
132+
"ensemble.invokeAPI('getDummyUser', null, { bypassCache: true }).then((res) => res.body.results[0].email)",
133+
),
134+
{ wrapper },
135+
);
136+
137+
let withoutForceInitialResult;
138+
let withoutForceResult;
139+
let withForceResult;
140+
141+
await act(async () => {
142+
withoutForceInitialResult = await withoutForce.current?.callback();
143+
withoutForceResult = await withoutForce.current?.callback();
144+
withForceResult = await withForce.current?.callback();
145+
});
146+
147+
expect(withoutForceInitialResult).toBe(withoutForceResult);
148+
expect(withForceResult).not.toBe(withoutForceResult);
149+
});
150+
114151
test.todo("populates application invokables");
115152

116153
test.todo("resolves values in order of scoping");

packages/runtime/src/runtime/hooks/__tests__/useInvokeApi.test.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,70 @@ test("after API response modal should close", async () => {
270270
expect(triggerAPIButton).not.toBeInTheDocument();
271271
});
272272
});
273+
274+
test("fetch API with force cache clear", async () => {
275+
fetchMock.mockResolvedValue({ body: { data: "foobar" } });
276+
277+
render(
278+
<EnsembleScreen
279+
screen={{
280+
name: "test_force_cache_clear",
281+
id: "test_force_cache_clear",
282+
body: {
283+
name: "Column",
284+
properties: {
285+
children: [
286+
{
287+
name: "Button",
288+
properties: {
289+
label: "Without Force",
290+
onTap: { invokeAPI: { name: "testForceCache" } },
291+
},
292+
},
293+
{
294+
name: "Button",
295+
properties: {
296+
label: "With Force",
297+
onTap: {
298+
invokeAPI: {
299+
name: "testForceCache",
300+
bypassCache: true,
301+
},
302+
},
303+
},
304+
},
305+
],
306+
},
307+
},
308+
apis: [
309+
{
310+
name: "testForceCache",
311+
method: "GET",
312+
cacheExpirySeconds: 60,
313+
},
314+
],
315+
}}
316+
/>,
317+
{ wrapper: BrowserRouterWrapper },
318+
);
319+
320+
const withoutForce = screen.getByText("Without Force");
321+
fireEvent.click(withoutForce);
322+
323+
await waitFor(() => {
324+
expect(fetchMock).toHaveBeenCalledTimes(1);
325+
});
326+
327+
fireEvent.click(withoutForce);
328+
329+
await waitFor(() => {
330+
expect(fetchMock).toHaveBeenCalledTimes(1);
331+
});
332+
333+
const withForce = screen.getByText("With Force");
334+
fireEvent.click(withForce);
335+
336+
await waitFor(() => {
337+
expect(fetchMock).toHaveBeenCalledTimes(2);
338+
});
339+
});

packages/runtime/src/runtime/hooks/useEnsembleAction.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,10 @@ export const useInvokeAPI: EnsembleActionHook<InvokeAPIAction> = (action) => {
233233
useMockResponse,
234234
},
235235
),
236-
staleTime: currentApi.cacheExpirySeconds
237-
? currentApi.cacheExpirySeconds * 1000
238-
: 0,
236+
staleTime:
237+
currentApi.cacheExpirySeconds && !action.bypassCache
238+
? currentApi.cacheExpirySeconds * 1000
239+
: 0,
239240
});
240241

241242
setData(currentApi.name, response);

0 commit comments

Comments
 (0)