@applicaster/zapp-react-native-utils 16.0.0-rc.20 → 16.0.0-rc.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
- package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
- package/pipesUtils/index.ts +1 -0
- package/pipesUtils/withPipesEndpoint.tsx +54 -0
- package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
- package/reactHooks/feed/index.ts +5 -1
- package/reactHooks/feed/useBatchLoading.ts +9 -1
- package/reactHooks/feed/{usePipesCacheReset.ts → useMarkPipesDataStale.ts} +12 -4
- package/reactHooks/utils/index.ts +3 -2
- package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "16.0.0-rc.
|
|
3
|
+
"version": "16.0.0-rc.22",
|
|
4
4
|
"description": "Applicaster Zapp React Native utilities package",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@applicaster/applicaster-types": "16.0.0-rc.
|
|
30
|
+
"@applicaster/applicaster-types": "16.0.0-rc.22",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { buildUrlWithQuery } from "../withPipesEndpoint";
|
|
2
|
+
|
|
3
|
+
describe("buildUrlWithQuery", () => {
|
|
4
|
+
it("returns the url unchanged when requestParams is null", () => {
|
|
5
|
+
expect(buildUrlWithQuery("https://foo.com/path", null)).toBe(
|
|
6
|
+
"https://foo.com/path"
|
|
7
|
+
);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("returns the url unchanged when requestParams has no params", () => {
|
|
11
|
+
expect(buildUrlWithQuery("https://foo.com/path", { headers: {} })).toBe(
|
|
12
|
+
"https://foo.com/path"
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("returns the url unchanged when url is empty", () => {
|
|
17
|
+
expect(buildUrlWithQuery("", { params: { a: "1" } })).toBe("");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("appends params to a url with no existing query", () => {
|
|
21
|
+
expect(
|
|
22
|
+
buildUrlWithQuery("https://foo.com/path", { params: { token: "abc" } })
|
|
23
|
+
).toBe("https://foo.com/path?token=abc");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("merges params with an existing query string", () => {
|
|
27
|
+
expect(
|
|
28
|
+
buildUrlWithQuery("https://foo.com/path?existing=1", {
|
|
29
|
+
params: { token: "abc" },
|
|
30
|
+
})
|
|
31
|
+
).toBe("https://foo.com/path?existing=1&token=abc");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render } from "@testing-library/react-native";
|
|
3
|
+
import { Text } from "react-native";
|
|
4
|
+
|
|
5
|
+
const mockedUseBuildPipesUrl = jest.fn();
|
|
6
|
+
|
|
7
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks/feed", () => ({
|
|
8
|
+
useBuildPipesUrl: (args) => mockedUseBuildPipesUrl(args),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const { withPipesEndpoint } = require("../withPipesEndpoint");
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line react/display-name
|
|
14
|
+
const Wrapped = React.forwardRef((props: any, _ref) => (
|
|
15
|
+
<Text testID="wrapped">{`${props.uri}|${JSON.stringify(props.headers)}`}</Text>
|
|
16
|
+
));
|
|
17
|
+
|
|
18
|
+
describe("withPipesEndpoint", () => {
|
|
19
|
+
afterEach(() => jest.clearAllMocks());
|
|
20
|
+
|
|
21
|
+
it("renders nothing while the endpoint context is resolving", () => {
|
|
22
|
+
mockedUseBuildPipesUrl.mockReturnValue({ requestParams: null });
|
|
23
|
+
|
|
24
|
+
const Decorated = withPipesEndpoint(Wrapped);
|
|
25
|
+
const { queryByTestId } = render(<Decorated uri="https://foo.com" />);
|
|
26
|
+
|
|
27
|
+
expect(queryByTestId("wrapped")).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("passes the uri through unchanged when there are no params", () => {
|
|
31
|
+
mockedUseBuildPipesUrl.mockReturnValue({ requestParams: {} });
|
|
32
|
+
|
|
33
|
+
const Decorated = withPipesEndpoint(Wrapped);
|
|
34
|
+
const { getByTestId } = render(<Decorated uri="https://foo.com/path" />);
|
|
35
|
+
|
|
36
|
+
expect(getByTestId("wrapped").props.children).toBe(
|
|
37
|
+
"https://foo.com/path|{}"
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("merges resolved params into the uri and forwards headers", () => {
|
|
42
|
+
mockedUseBuildPipesUrl.mockReturnValue({
|
|
43
|
+
requestParams: {
|
|
44
|
+
params: { token: "abc" },
|
|
45
|
+
headers: { Authorization: "Bearer xyz" },
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const Decorated = withPipesEndpoint(Wrapped);
|
|
50
|
+
const { getByTestId } = render(<Decorated uri="https://foo.com/path" />);
|
|
51
|
+
|
|
52
|
+
expect(getByTestId("wrapped").props.children).toBe(
|
|
53
|
+
'https://foo.com/path?token=abc|{"Authorization":"Bearer xyz"}'
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { withPipesEndpoint, buildUrlWithQuery } from "./withPipesEndpoint";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { forwardRef, RefObject } from "react";
|
|
2
|
+
import URL from "url";
|
|
3
|
+
|
|
4
|
+
import { useBuildPipesUrl } from "@applicaster/zapp-react-native-utils/reactHooks/feed";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Merges the query params resolved from a pipes endpoint context into the url.
|
|
8
|
+
* Returns the url unchanged when there is nothing to merge.
|
|
9
|
+
*/
|
|
10
|
+
export function buildUrlWithQuery(url: string, requestParams): string {
|
|
11
|
+
if (!url || !requestParams?.params) {
|
|
12
|
+
return url;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parsedURL = URL.parse(url, true);
|
|
16
|
+
|
|
17
|
+
parsedURL.query = { ...parsedURL.query, ...requestParams.params };
|
|
18
|
+
parsedURL.search = null;
|
|
19
|
+
|
|
20
|
+
return URL.format(parsedURL);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Props = {
|
|
24
|
+
uri: string;
|
|
25
|
+
} & Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* HOC that resolves the wrapped component's `uri` against its matching Zapp
|
|
29
|
+
* Pipes endpoint. Endpoint context keys are added as query params (merged into
|
|
30
|
+
* the uri) and as request `headers`. Rendering is deferred until the context
|
|
31
|
+
* has been resolved so the component never loads without its required headers.
|
|
32
|
+
*/
|
|
33
|
+
export function withPipesEndpoint(Component) {
|
|
34
|
+
function WithPipesEndpoint(props: Props, ref: RefObject<unknown>) {
|
|
35
|
+
const { requestParams } = useBuildPipesUrl({ url: props.uri });
|
|
36
|
+
|
|
37
|
+
if (requestParams !== null) {
|
|
38
|
+
const urlWithQuery = buildUrlWithQuery(props.uri, requestParams);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Component
|
|
42
|
+
ref={ref}
|
|
43
|
+
{...props}
|
|
44
|
+
uri={urlWithQuery}
|
|
45
|
+
headers={requestParams.headers || {}}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return forwardRef(WithPipesEndpoint);
|
|
54
|
+
}
|
package/reactHooks/feed/index.ts
CHANGED
|
@@ -6,7 +6,11 @@ export { useEntryScreenId } from "./useEntryScreenId";
|
|
|
6
6
|
|
|
7
7
|
export { useBuildPipesUrl } from "./useBuildPipesUrl";
|
|
8
8
|
|
|
9
|
-
export {
|
|
9
|
+
export {
|
|
10
|
+
useMarkPipesDataStale,
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
12
|
+
usePipesCacheReset,
|
|
13
|
+
} from "./useMarkPipesDataStale";
|
|
10
14
|
|
|
11
15
|
export { useBatchLoading } from "./useBatchLoading";
|
|
12
16
|
|
|
@@ -159,9 +159,17 @@ export const useBatchLoading = (
|
|
|
159
159
|
options.riverId,
|
|
160
160
|
]);
|
|
161
161
|
|
|
162
|
+
// Initial preload only. Batch loading warms the first batch of component
|
|
163
|
+
// feeds and signals readiness; the feed set is frozen at mount on purpose.
|
|
164
|
+
// Per-component loads and reloads (including stale revalidation) are owned by
|
|
165
|
+
// useFeedLoader in ZappPipesDataConnector. Re-running on `feeds`/input changes
|
|
166
|
+
// would re-fire on every store update and cause redundant dispatch storms, so
|
|
167
|
+
// this intentionally runs once on mount.
|
|
168
|
+
/* eslint-disable @wogns3623/better-exhaustive-deps/exhaustive-deps */
|
|
162
169
|
React.useEffect(() => {
|
|
163
170
|
runBatchLoading();
|
|
164
|
-
}, [
|
|
171
|
+
}, []);
|
|
172
|
+
/* eslint-enable @wogns3623/better-exhaustive-deps/exhaustive-deps */
|
|
165
173
|
|
|
166
174
|
React.useEffect(() => {
|
|
167
175
|
// check if all feeds are ready and set hasEverBeenReady to true
|
|
@@ -2,17 +2,19 @@ import React from "react";
|
|
|
2
2
|
|
|
3
3
|
import { getDatasourceUrl } from "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/getDatasourceUrl";
|
|
4
4
|
import { usePipesContexts } from "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/usePipesContexts";
|
|
5
|
-
import {
|
|
5
|
+
import { markPipesDataStale } from "@applicaster/zapp-react-native-redux/ZappPipes";
|
|
6
6
|
|
|
7
7
|
import { useRoute } from "../navigation";
|
|
8
8
|
import { useAppDispatch } from "@applicaster/zapp-react-native-redux";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Mark a river's `clear_cache_on_reload` feeds as stale when the screen is
|
|
12
|
+
* unmounted. The cached data is kept (so a screen sharing the same feed does
|
|
13
|
+
* not lose it mid-mount) and revalidated on next access.
|
|
12
14
|
* @param {string} riverId screen id
|
|
13
15
|
* @param {Array} riverComponents list of UI components
|
|
14
16
|
*/
|
|
15
|
-
export const
|
|
17
|
+
export const useMarkPipesDataStale = (riverId, riverComponents) => {
|
|
16
18
|
const dispatch = useAppDispatch();
|
|
17
19
|
const { screenData, pathname } = useRoute();
|
|
18
20
|
const pipesContexts = usePipesContexts(riverId, pathname);
|
|
@@ -35,10 +37,16 @@ export const usePipesCacheReset = (riverId, riverComponents) => {
|
|
|
35
37
|
);
|
|
36
38
|
|
|
37
39
|
if (url) {
|
|
38
|
-
dispatch(
|
|
40
|
+
dispatch(markPipesDataStale(url, { riverId }));
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
});
|
|
42
44
|
};
|
|
43
45
|
}, []);
|
|
44
46
|
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @deprecated Renamed to `useMarkPipesDataStale`. Kept as an alias for backward
|
|
50
|
+
* compatibility with external consumers.
|
|
51
|
+
*/
|
|
52
|
+
export const usePipesCacheReset = useMarkPipesDataStale;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* This hook returns a previous value that was passed to it.
|
|
@@ -40,6 +40,7 @@ export const shouldDispatchData = (
|
|
|
40
40
|
) => {
|
|
41
41
|
const currentFeedHasData = feed?.data;
|
|
42
42
|
const isLocalFeed = checkIsLocalFeed(url);
|
|
43
|
+
const isFeedStale = feed?.stale;
|
|
43
44
|
|
|
44
|
-
return !currentFeedHasData || clearCache || isLocalFeed;
|
|
45
|
+
return !currentFeedHasData || clearCache || isFeedStale || isLocalFeed;
|
|
45
46
|
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const usePipesCacheReset = jest.fn();
|