Skip to content

Commit 6f4a739

Browse files
committed
add cross tab browser invalidate query support
1 parent cfccff7 commit 6f4a739

6 files changed

Lines changed: 86 additions & 8 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@povio/openapi-codegen-cli",
3-
"version": "2.0.7",
3+
"version": "2.0.8-rc.1",
44
"keywords": [
55
"codegen",
66
"openapi",
@@ -119,4 +119,4 @@
119119
"yarn": ">= 3.2"
120120
},
121121
"packageManager": "yarn@4.2.2"
122-
}
122+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { QueryClient, QueryKey } from "@tanstack/react-query";
2+
3+
const CROSS_TAB_INVALIDATE_KEY = "__rq_invalidate__";
4+
5+
/**
6+
* Broadcasts a query invalidation event to all other open tabs via localStorage.
7+
*
8+
* @param queryKeys - An array of query keys to invalidate (array of arrays).
9+
*
10+
* NOTE: The `storage` event only fires in *other* tabs — the calling tab
11+
* must invalidate its own queryClient separately if needed.
12+
*/
13+
export const broadcastQueryInvalidation = (queryKeys: QueryKey[]) => {
14+
localStorage.setItem(CROSS_TAB_INVALIDATE_KEY, JSON.stringify({ keys: queryKeys, timestamp: Date.now() }));
15+
};
16+
17+
/**
18+
* Registers a one-time global `storage` event listener that reacts to
19+
* cross-tab invalidation broadcasts. Safe to call from multiple hooks —
20+
* only the first call sets up the listener.
21+
*/
22+
let isListenerSetUp = false;
23+
24+
export const setupCrossTabListener = (queryClient: QueryClient) => {
25+
if (isListenerSetUp) return;
26+
isListenerSetUp = true;
27+
28+
window.addEventListener("storage", (e: StorageEvent) => {
29+
if (e.key !== CROSS_TAB_INVALIDATE_KEY || !e.newValue) return;
30+
31+
try {
32+
const { keys } = JSON.parse(e.newValue) as { keys: QueryKey[] };
33+
for (const queryKey of keys) {
34+
queryClient.invalidateQueries({ queryKey });
35+
}
36+
} catch {
37+
// Ignore malformed payloads
38+
}
39+
});
40+
};

src/assets/useMutationEffects.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { useCallback } from "react";
1+
import { useCallback, useEffect } from "react";
22

33
import { OpenApiQueryConfig } from "@povio/openapi-codegen-cli";
44
import { QueryKey, useQueryClient } from "@tanstack/react-query";
55

66
import { QueryModule } from "./queryModules";
7+
import { broadcastQueryInvalidation, setupCrossTabListener } from "./useCrossTabQueryInvalidation";
78

89
export interface MutationEffectsOptions {
910
invalidateCurrentModule?: boolean;
11+
crossTabInvalidation?: boolean;
12+
invalidationMap?: Record<string, (context: Record<string, string>) => QueryKey[]>;
1013
invalidateModules?: QueryModule[];
1114
invalidateKeys?: QueryKey[];
1215
preferUpdate?: boolean;
@@ -20,6 +23,11 @@ export function useMutationEffects({ currentModule }: UseMutationEffectsProps) {
2023
const queryClient = useQueryClient();
2124
const config = OpenApiQueryConfig.useConfig();
2225

26+
useEffect(() => {
27+
if (!config.crossTabInvalidation) return;
28+
setupCrossTabListener(queryClient);
29+
}, [queryClient, config.crossTabInvalidation]);
30+
2331
const runMutationEffects = useCallback(
2432
async <TData>(data: TData, options: MutationEffectsOptions = {}, updateKeys?: QueryKey[]) => {
2533
const { invalidateCurrentModule = true, invalidateModules, invalidateKeys, preferUpdate } = options;
@@ -30,6 +38,8 @@ export function useMutationEffects({ currentModule }: UseMutationEffectsProps) {
3038
const isQueryKeyEqual = (keyA: QueryKey, keyB: QueryKey) =>
3139
keyA.length === keyB.length && keyA.every((item, index) => item === keyB[index]);
3240

41+
const invalidatedQueryKeys: QueryKey[] = [];
42+
3343
queryClient.invalidateQueries({
3444
predicate: ({ queryKey }) => {
3545
const isUpdateKey = updateKeys?.some((key) => isQueryKeyEqual(queryKey, key));
@@ -40,15 +50,29 @@ export function useMutationEffects({ currentModule }: UseMutationEffectsProps) {
4050
const isCurrentModule = shouldInvalidateCurrentModule && queryKey[0] === currentModule;
4151
const isInvalidateModule = !!invalidateModules && invalidateModules.some((module) => queryKey[0] === module);
4252
const isInvalidateKey = !!invalidateKeys && invalidateKeys.some((key) => isQueryKeyEqual(queryKey, key));
43-
return isCurrentModule || isInvalidateModule || isInvalidateKey;
53+
54+
const map = config.invalidationMap?.[currentModule]?.(data);
55+
const isMappedKey = !!map && map.some((key) => isQueryKeyEqual(queryKey, key));
56+
57+
const shouldInvalidate = isCurrentModule || isInvalidateModule || isInvalidateKey || isMappedKey;
58+
59+
if (shouldInvalidate && config.crossTabInvalidation) {
60+
invalidatedQueryKeys.push([...queryKey]);
61+
}
62+
63+
return shouldInvalidate;
4464
},
4565
});
4666

67+
if (config.crossTabInvalidation && invalidatedQueryKeys.length > 0) {
68+
broadcastQueryInvalidation(invalidatedQueryKeys);
69+
}
70+
4771
if (shouldUpdate && updateKeys) {
4872
updateKeys.map((queryKey) => queryClient.setQueryData(queryKey, data));
4973
}
5074
},
51-
[queryClient, currentModule, config.preferUpdate],
75+
[queryClient, currentModule, config.preferUpdate, config.invalidationMap, config.crossTabInvalidation],
5276
);
5377

5478
return { runMutationEffects };

src/generators/const/deps.const.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ export const MUTATION_EFFECTS = {
4646
};
4747
export const MUTATION_EFFECTS_FILE: GenerateFile = { fileName: "useMutationEffects", extension: "ts" };
4848

49+
// CrossTabQueryInvalidation
50+
export const CROSS_TAB_QUERY_INVALIDATION_FILE: GenerateFile = {
51+
fileName: "useCrossTabQueryInvalidation",
52+
extension: "ts",
53+
};
54+
4955
// ZodExtended
5056
export const ZOD_EXTENDED = {
5157
namespace: "ZodExtended",

src/generators/utils/generate-files.utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ACL_APP_ABILITY_FILE, ACL_CHECK_FILE } from "@/generators/const/acl.const";
22
import {
33
APP_REST_CLIENT_FILE,
4+
CROSS_TAB_QUERY_INVALIDATION_FILE,
45
MUTATION_EFFECTS_FILE,
56
QUERY_MODULES_FILE,
67
ZOD_EXTENDED_FILE,
@@ -50,7 +51,7 @@ export function getMutationEffectsFiles(data: GenerateData, resolver: SchemaReso
5051
}
5152

5253
return [
53-
...getAssetFiles([MUTATION_EFFECTS_FILE], resolver),
54+
...getAssetFiles([MUTATION_EFFECTS_FILE, CROSS_TAB_QUERY_INVALIDATION_FILE], resolver),
5455
{
5556
fileName: getOutputFileName({
5657
output: resolver.options.output,

src/lib/config/queryConfig.context.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1+
import type { QueryKey } from "@tanstack/react-query";
12
import { createContext, use, useMemo } from "react";
23
import { PropsWithChildren } from "react";
34

45
export namespace OpenApiQueryConfig {
56
interface Type {
67
preferUpdate?: boolean;
78
invalidateCurrentModule?: boolean;
9+
invalidationMap?: Record<string, (context: Record<string, string>) => QueryKey[]>;
10+
crossTabInvalidation?: boolean;
811
}
912

1013
const Context = createContext<Type>({});
1114

1215
type ProviderProps = Type;
1316

14-
export const Provider = ({ preferUpdate, invalidateCurrentModule, children }: PropsWithChildren<ProviderProps>) => {
15-
const value = useMemo(() => ({ preferUpdate, invalidateCurrentModule }), [preferUpdate, invalidateCurrentModule]);
17+
export const Provider = ({ preferUpdate, invalidateCurrentModule,
18+
invalidationMap,
19+
crossTabInvalidation,
20+
21+
children }: PropsWithChildren<ProviderProps>) => {
22+
const value = useMemo(() => ({ preferUpdate, invalidateCurrentModule, invalidationMap, crossTabInvalidation }), [preferUpdate, invalidateCurrentModule, invalidationMap , crossTabInvalidation]);
1623

1724
return <Context.Provider value={value}>{children}</Context.Provider>;
1825
};

0 commit comments

Comments
 (0)