Skip to content

Commit fb94d0a

Browse files
feat(fetchye-one-app): add streaming support
1 parent e5426fe commit fb94d0a

9 files changed

Lines changed: 168 additions & 0 deletions

File tree

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,57 @@ A promise resolving to an object with the below keys:
957957
| `error?` | `Object` | An object containing an error if present. *Defaults to an `Error` object with a thrown `fetch` error. This is not for API errors (e.g. Status 500 or 400). See `data` for that* |
958958
| `run` | `async () => {}` | A function for bypassing the cache and firing an API call. Can be awaited. |
959959
960+
### streamedFetchye
961+
962+
A helper to enable streaming for server side Fetchye API calls. It can be used the same way as [`oneFetchye`](https://github.com/americanexpress/fetchye?tab=readme-ov-file#oneFetchye).
963+
964+
```js
965+
import { streamedFetchye } from 'fetchye-one-app';
966+
967+
const loadModuleData = async ({ store: { dispatch } }) => dispatch(streamedFetchye('https://example.com/api/v2/people', {
968+
headers: {
969+
'Content-Type': 'application/json',
970+
},
971+
}));
972+
```
973+
974+
### useStreamedFetchye
975+
976+
A React hook used to read streamed data from the server. This hook will throw an error if data does not exist under the requested key.
977+
978+
Note: The hook must be used within a suspense boundary.
979+
980+
```jsx
981+
import { useStreamedFetchye } from 'fetchye-one-app';
982+
import { Spinner } from 'design-library';
983+
984+
const MyComponent = () => {
985+
const {
986+
data,
987+
} = useStreamedFetchye('https://example.com/api/v2/people', {
988+
headers: {
989+
'Content-Type': 'application/json',
990+
},
991+
}
992+
);
993+
994+
return (
995+
<>
996+
<p>{data.body.name}</p>
997+
</>
998+
);
999+
};
1000+
1001+
const Container = () => {
1002+
return (
1003+
<Suspense fallback={<Spinner size="sm" />}>
1004+
<MyComponent />
1005+
</Suspense>
1006+
)
1007+
}
1008+
1009+
```
1010+
9601011
### Providers
9611012
9621013
A Provider creates a React Context to connect all the `useFetchye` Hooks into a centrally stored cache.

packages/fetchye-one-app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"@babel/cli": "^7.12.1",
5656
"@babel/core": "7.11.6",
5757
"@testing-library/react": "^11.0.4",
58+
"@testing-library/react-hooks": "^3.4.2",
5859
"babel-preset-amex": "^3.4.1",
5960
"cross-env": "^7.0.2",
6061
"fetchye-core": "^1.8.0",
@@ -63,6 +64,7 @@
6364
"react-dom": "^17.0.2",
6465
"react-redux": "^7.2.2",
6566
"redux": "^4.0.5",
67+
"redux-thunk": "^2.4.2",
6668
"rimraf": "^3.0.2"
6769
}
6870
}

packages/fetchye-one-app/src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ export { makeOneServerFetchye } from './makeOneServerFetchye';
1818
export OneCache, { oneCacheSelector } from './OneCache';
1919
export OneFetchyeProvider from './OneFetchyeProvider';
2020
export oneFetchye from './oneFetchye';
21+
// streaming
22+
export { streamedFetchye } from './streaming/streamedFetchye';
23+
export { useStreamedFetchye } from './streaming/useStreamedFetchye';
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export const storeLocalPromise = (domain, key, promise) => (_a, _b, { promiseStore }) => {
2+
promiseStore?.storeLocalPromise(domain, key, promise);
3+
};
4+
5+
export const getLocalPromise = (domain, key) => (_a, _b, { promiseStore }) => promiseStore?.getLocalPromise(domain, key);
6+
7+
export const getStreamingPromises = () => (_a, _b, { promiseStore }) => {
8+
return promiseStore?.getStreamingPromises() ?? [];
9+
}
10+
11+
export const stream = (promiseArray) => (_a, _b, { promiseStore }) => {
12+
if (!Array.isArray(promiseArray)) {
13+
throw new TypeError('promiseArray must be an array');
14+
}
15+
16+
promiseArray.forEach(({ domain, key, promise }) => {
17+
const storeDomain = domain ?? key;
18+
if (domain && typeof domain !== 'string') {
19+
throw new TypeError('domain must be a string');
20+
}
21+
22+
if (typeof key !== 'string') {
23+
throw new TypeError('key must be a string');
24+
}
25+
26+
if (!(promise instanceof Promise)) {
27+
throw new TypeError('promise must be an instance of Promise');
28+
}
29+
30+
promiseStore.storeStreamingPromise(storeDomain, key, promise);
31+
});
32+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const STREAM_DOMAIN = '__stream__';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// eslint-disable-next-line import/no-extraneous-dependencies -- transitive from fetchye-redux-provider
2+
import { useDispatch } from 'react-redux';
3+
import { getStreamingPromises } from './actions';
4+
import { STREAM_DOMAIN } from './constants';
5+
6+
export const useStreamedPromise = (key, domain = STREAM_DOMAIN) => {
7+
const dispatch = useDispatch();
8+
const promises = dispatch(getStreamingPromises());
9+
return promises?.find((p) => p.key === key && p.domain === (domain || key))?.promise;
10+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { computeKey } from 'fetchye';
2+
import { stream } from './actions';
3+
import { STREAM_DOMAIN } from './constants';
4+
5+
export const streamedFetchye = (fetchyeThunk, key, options = {}) => async (dispatch) => {
6+
const { hash: computedKey } = computeKey(key, options);
7+
const promise = dispatch(fetchyeThunk);
8+
9+
dispatch(
10+
stream([
11+
{
12+
key: computedKey,
13+
domain: STREAM_DOMAIN,
14+
promise,
15+
},
16+
])
17+
);
18+
19+
return promise;
20+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// eslint-disable-next-line import/no-extraneous-dependencies -- transitive from fetchye-redux-provider
2+
import { useDispatch } from 'react-redux';
3+
import { computeKey } from 'fetchye';
4+
import { useCallback, useMemo } from 'react';
5+
import { STREAM_DOMAIN } from './constants';
6+
import oneFetchye from '../oneFetchye';
7+
import { useStreamedPromise } from './hooks';
8+
import { getLocalPromise, storeLocalPromise } from './actions';
9+
10+
export const useStreamedFetchye = (
11+
key,
12+
options = {},
13+
fetcher = undefined
14+
) => {
15+
const dispatch = useDispatch();
16+
const { hash: promiseStoreKey } = computeKey(key, options);
17+
const serverPromise = useStreamedPromise(promiseStoreKey);
18+
19+
const makeClientRequest = useCallback(() => {
20+
const localPromise = dispatch(getLocalPromise(STREAM_DOMAIN, promiseStoreKey));
21+
if (localPromise) {
22+
return localPromise;
23+
}
24+
25+
const promise = dispatch(oneFetchye(key, options, fetcher));
26+
27+
dispatch(storeLocalPromise(
28+
STREAM_DOMAIN,
29+
promiseStoreKey,
30+
promise
31+
));
32+
33+
return promise;
34+
}, [key, options, fetcher, promiseStoreKey, dispatch]);
35+
36+
const shouldUseLocalPromise = !serverPromise;
37+
38+
const localPromise = useMemo(() => {
39+
if (!shouldUseLocalPromise) return null;
40+
return makeClientRequest();
41+
}, [shouldUseLocalPromise, makeClientRequest]);
42+
43+
return localPromise || serverPromise;
44+
};

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9040,6 +9040,11 @@ redent@^3.0.0:
90409040
indent-string "^4.0.0"
90419041
strip-indent "^3.0.0"
90429042

9043+
redux-thunk@^2.4.2:
9044+
version "2.4.2"
9045+
resolved "https://artifactory.aexp.com/artifactory/api/npm/npm-virtual/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b"
9046+
integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==
9047+
90439048
redux@^4.0.5:
90449049
version "4.0.5"
90459050
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"

0 commit comments

Comments
 (0)