Skip to content

Commit 70b9dd4

Browse files
authored
feat: add normalize (#40)
1 parent 16f57eb commit 70b9dd4

22 files changed

+1874
-28
lines changed

package-lock.json

Lines changed: 20 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"typecheck": "tsc --noEmit"
4444
},
4545
"dependencies": {
46+
"@normy/core": "^0.14.0",
4647
"utility-types": "^3.11.0"
4748
},
4849
"devDependencies": {

src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type {
1818
export type {DataManager} from './types/DataManager';
1919
export type {DataLoaderStatus} from './types/DataLoaderStatus';
2020
export type {InvalidateRepeatOptions, InvalidateOptions} from './types/DataManagerOptions';
21+
export type {Normalizer, NormalizerConfig, OptimisticConfig} from './types/Normalizer';
2122

2223
export {idle} from './constants';
2324

src/core/types/DataManager.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
import type {Data} from '@normy/core';
2+
13
import type {InvalidateOptions} from './DataManagerOptions';
24
import type {AnyDataSource, DataSourceParams, DataSourceTag} from './DataSource';
5+
import type {Normalizer} from './Normalizer';
36

47
export interface DataManager {
8+
normalizer?: Normalizer;
9+
10+
optimisticUpdate(mutationData: Data): void;
11+
12+
invalidateData(data: Data): void;
13+
514
invalidateTag(tag: DataSourceTag, invalidateOptions?: InvalidateOptions): Promise<void>;
615

716
invalidateTags(tags: DataSourceTag[], invalidateOptions?: InvalidateOptions): Promise<void>;

src/core/types/Normalizer.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type {NormalizerConfig as NormalizeConfigBase} from '@normy/core';
2+
import type {Data, NormalizedData} from '@normy/core/types/types';
3+
4+
export interface OptimisticConfig {
5+
/** Automatically calculate rollback data, defaults to true */
6+
autoCalculateRollback?: boolean;
7+
/** Whether debug logging is enabled */
8+
devLogging?: boolean;
9+
}
10+
11+
export interface NormalizerConfig extends NormalizeConfigBase {
12+
initialData?: NormalizedData;
13+
optimistic?: boolean | OptimisticConfig;
14+
invalidate?: boolean;
15+
}
16+
17+
export interface Normalizer {
18+
getNormalizedData: () => NormalizedData;
19+
clearNormalizedData: () => void;
20+
setQuery: (queryKey: string, queryData: Data) => void;
21+
removeQuery: (queryKey: string) => void;
22+
getQueriesToUpdate: (mutationData: Data) => {
23+
queryKey: string;
24+
data: Data;
25+
}[];
26+
getObjectById: <T extends Data>(id: string, exampleObject?: T) => T | undefined;
27+
getQueryFragment: <T extends Data>(fragment: Data, exampleObject?: T) => T | undefined;
28+
getDependentQueries: (mutationData: Data) => readonly string[];
29+
getDependentQueriesByIds: (ids: ReadonlyArray<string>) => readonly string[];
30+
getCurrentData: <T extends Data>(newData: T) => T | undefined;
31+
log: (...messages: unknown[]) => void;
32+
}

src/react-query/ClientDataManager.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
1-
import type {InvalidateQueryFilters, QueryClientConfig} from '@tanstack/react-query';
1+
import type {Data} from '@normy/core';
2+
import {createNormalizer} from '@normy/core';
3+
import type {InvalidateQueryFilters, QueryClientConfig, QueryKey} from '@tanstack/react-query';
24
import {QueryClient} from '@tanstack/react-query';
35

46
import {
57
type AnyDataSource,
68
type DataManager,
79
type DataSourceParams,
810
type DataSourceTag,
11+
type Normalizer,
12+
type NormalizerConfig,
913
composeFullKey,
1014
hasTag,
1115
} from '../core';
1216
import type {InvalidateOptions, InvalidateRepeatOptions} from '../core/types/DataManagerOptions';
1317

14-
export type ClientDataManagerConfig = QueryClientConfig;
18+
import type {QueryNormalizer} from './types/normalizer';
19+
import {createQueryNormalizer} from './utils/normalize';
20+
21+
export interface ClientDataManagerConfig extends QueryClientConfig {
22+
normalizerConfig?: NormalizerConfig | boolean;
23+
}
1524

1625
export class ClientDataManager implements DataManager {
1726
readonly queryClient: QueryClient;
27+
readonly normalizer?: Normalizer | undefined;
28+
readonly queryNormalizer?: QueryNormalizer | undefined;
1829

1930
constructor(config: ClientDataManagerConfig = {}) {
2031
this.queryClient = new QueryClient({
@@ -31,6 +42,53 @@ export class ClientDataManager implements DataManager {
3142
},
3243
},
3344
});
45+
46+
this.normalizer = this.createNormalize(config.normalizerConfig);
47+
this.queryNormalizer = createQueryNormalizer(
48+
this.normalizer,
49+
this.queryClient,
50+
config.normalizerConfig,
51+
(data) => this.optimisticUpdate(data),
52+
(data) => this.invalidateData(data),
53+
);
54+
}
55+
56+
optimisticUpdate(mutationData: Data) {
57+
if (!this.normalizer) {
58+
return;
59+
}
60+
61+
const queriesToUpdate = this.normalizer.getQueriesToUpdate(mutationData);
62+
63+
queriesToUpdate.forEach((query) => {
64+
const queryKey = JSON.parse(query.queryKey) as QueryKey;
65+
66+
const cachedQuery = this.queryClient.getQueryCache().find({queryKey});
67+
68+
const dataUpdatedAt = cachedQuery?.state.dataUpdatedAt;
69+
const isInvalidated = cachedQuery?.state.isInvalidated;
70+
const error = cachedQuery?.state.error;
71+
const status = cachedQuery?.state.status;
72+
73+
this.queryClient.setQueryData(queryKey, () => query.data, {
74+
updatedAt: dataUpdatedAt,
75+
});
76+
77+
cachedQuery?.setState({isInvalidated, error, status});
78+
});
79+
}
80+
81+
invalidateData(data: Data): void {
82+
if (!this.normalizer) {
83+
return;
84+
}
85+
86+
const queriesToUpdate = this.normalizer.getQueriesToUpdate(data);
87+
88+
queriesToUpdate.forEach((query) => {
89+
const queryKey = JSON.parse(query.queryKey) as QueryKey;
90+
this.queryClient.invalidateQueries({queryKey});
91+
});
3492
}
3593

3694
invalidateTag(tag: DataSourceTag, invalidateOptions?: InvalidateOptions) {
@@ -129,4 +187,18 @@ export class ClientDataManager implements DataManager {
129187
setTimeout(invalidate, repeat.interval * i);
130188
}
131189
}
190+
191+
private createNormalize(
192+
config: boolean | NormalizerConfig | undefined,
193+
): Normalizer | undefined {
194+
if (!config) {
195+
return undefined;
196+
}
197+
198+
if (config === true) {
199+
return createNormalizer({});
200+
}
201+
202+
return createNormalizer(config);
203+
}
132204
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
3+
import {QueryClientProvider} from '@tanstack/react-query';
4+
5+
import {DataManagerProvider} from '../react/DataManagerProvider';
6+
7+
import type {ClientDataManager} from './ClientDataManager';
8+
9+
export interface DataSourceProviderProps {
10+
dataManager: ClientDataManager;
11+
children: React.ReactNode;
12+
}
13+
14+
export const DataSourceProvider: React.FC<DataSourceProviderProps> = ({children, dataManager}) => {
15+
React.useEffect(() => {
16+
if (!dataManager.queryNormalizer) {
17+
return undefined;
18+
}
19+
20+
dataManager.queryNormalizer.subscribe();
21+
22+
return () => {
23+
dataManager.queryNormalizer?.unsubscribe();
24+
dataManager.queryNormalizer?.clear();
25+
};
26+
// eslint-disable-next-line react-hooks/exhaustive-deps
27+
}, []);
28+
29+
return (
30+
<QueryClientProvider client={dataManager.queryClient}>
31+
<DataManagerProvider dataManager={dataManager}>{children}</DataManagerProvider>
32+
</QueryClientProvider>
33+
);
34+
};

0 commit comments

Comments
 (0)