@terreno/rtk 0.26.0 → 0.27.0
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/dist/authSlice.d.ts.map +1 -1
- package/dist/authSlice.js +2 -0
- package/dist/authSlice.js.map +1 -1
- package/dist/authSlice.test.js +4 -1
- package/dist/authSlice.test.js.map +1 -1
- package/dist/authSliceNative.test.js +3 -16
- package/dist/authSliceNative.test.js.map +1 -1
- package/dist/betterAuthClient.d.ts.map +1 -1
- package/dist/betterAuthClient.js +0 -1
- package/dist/betterAuthClient.js.map +1 -1
- package/dist/betterAuthSlice.test.js +1 -0
- package/dist/betterAuthSlice.test.js.map +1 -1
- package/dist/emptyApi.d.ts.map +1 -1
- package/dist/emptyApi.js +0 -2
- package/dist/emptyApi.js.map +1 -1
- package/dist/isolated/constants.isolated.d.ts +2 -0
- package/dist/isolated/constants.isolated.d.ts.map +1 -0
- package/dist/isolated/constants.isolated.js +81 -0
- package/dist/isolated/constants.isolated.js.map +1 -0
- package/dist/isolated/offlineMiddleware.isolated.js +0 -1
- package/dist/isolated/offlineMiddleware.isolated.js.map +1 -1
- package/dist/isolated/offlineMiddleware.native.isolated.js +0 -1
- package/dist/isolated/offlineMiddleware.native.isolated.js.map +1 -1
- package/dist/isolated/useUpgradeCheck.mobile.isolated.js +47 -0
- package/dist/isolated/useUpgradeCheck.mobile.isolated.js.map +1 -1
- package/dist/offlineGate.d.ts +1 -1
- package/dist/offlineGate.d.ts.map +1 -1
- package/dist/offlineGate.js +1 -3
- package/dist/offlineGate.js.map +1 -1
- package/dist/offlineMiddleware.d.ts.map +1 -1
- package/dist/offlineMiddleware.js +26 -0
- package/dist/offlineMiddleware.js.map +1 -1
- package/dist/offlineMiddleware.test.js +5 -1
- package/dist/offlineMiddleware.test.js.map +1 -1
- package/dist/realtime.test.js +1 -0
- package/dist/realtime.test.js.map +1 -1
- package/dist/sessionApi.test.d.ts +2 -0
- package/dist/sessionApi.test.d.ts.map +1 -0
- package/dist/sessionApi.test.js +84 -0
- package/dist/sessionApi.test.js.map +1 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js.map +1 -1
- package/dist/sync.test.js +1 -3
- package/dist/sync.test.js.map +1 -1
- package/dist/testPreload.test.js +6 -0
- package/dist/testPreload.test.js.map +1 -1
- package/dist/useFeatureFlags.d.ts.map +1 -1
- package/dist/useFeatureFlags.js.map +1 -1
- package/dist/useTerrenoFeatureFlags.d.ts.map +1 -1
- package/dist/useTerrenoFeatureFlags.js.map +1 -1
- package/dist/useTerrenoFeatureFlags.test.js +10 -0
- package/dist/useTerrenoFeatureFlags.test.js.map +1 -1
- package/dist/useUpgradeCheckNative.test.js +21 -0
- package/dist/useUpgradeCheckNative.test.js.map +1 -1
- package/package.json +2 -2
- package/src/authSlice.test.ts +5 -1
- package/src/authSlice.ts +2 -0
- package/src/authSliceNative.test.ts +4 -18
- package/src/betterAuthClient.ts +1 -2
- package/src/betterAuthSlice.test.ts +1 -0
- package/src/emptyApi.ts +5 -4
- package/src/isolated/constants.isolated.ts +118 -0
- package/src/isolated/offlineMiddleware.isolated.ts +1 -2
- package/src/isolated/offlineMiddleware.native.isolated.ts +1 -2
- package/src/isolated/useUpgradeCheck.mobile.isolated.ts +60 -0
- package/src/offlineGate.ts +1 -2
- package/src/offlineMiddleware.test.ts +6 -1
- package/src/offlineMiddleware.ts +27 -0
- package/src/realtime.test.ts +1 -0
- package/src/sessionApi.test.ts +115 -0
- package/src/sync.test.ts +1 -2
- package/src/sync.ts +2 -1
- package/src/testPreload.test.ts +7 -0
- package/src/useFeatureFlags.ts +1 -0
- package/src/useTerrenoFeatureFlags.test.ts +16 -0
- package/src/useTerrenoFeatureFlags.ts +2 -0
- package/src/useUpgradeCheckNative.test.ts +26 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import {afterAll, afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
|
|
3
2
|
|
|
4
3
|
// Keep this mock a superset of the preload's react-native mock so that other
|
|
5
4
|
// test files (e.g. useUpgradeCheck.test.ts) can still find AppState and Linking
|
|
@@ -17,7 +16,8 @@ mock.module("react-native", () => ({
|
|
|
17
16
|
|
|
18
17
|
// Force IsWeb=false regardless of whether ./platform was already imported
|
|
19
18
|
// elsewhere in the test run. `mock.module` is hoisted in bun, so this takes
|
|
20
|
-
// effect before the dynamic imports below.
|
|
19
|
+
// effect before the dynamic imports below. Web-path test files must set their
|
|
20
|
+
// own top-of-file `IsWeb: true` guard; do not rely on afterAll restore here.
|
|
21
21
|
mock.module("./platform", () => ({IsWeb: false}));
|
|
22
22
|
|
|
23
23
|
const secureCalls = {
|
|
@@ -69,7 +69,7 @@ const api = createApi({
|
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
const createTestStore = () => {
|
|
72
|
-
const {authReducer, middleware, authSlice} = auth.generateAuthSlice(api
|
|
72
|
+
const {authReducer, middleware, authSlice} = auth.generateAuthSlice(api);
|
|
73
73
|
const store = configureStore({
|
|
74
74
|
middleware: (getDefault) =>
|
|
75
75
|
getDefault({serializableCheck: false}).concat(api.middleware, ...middleware),
|
|
@@ -111,20 +111,6 @@ describe("native listener middleware side effects", () => {
|
|
|
111
111
|
console.error = originalError;
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
afterAll(() => {
|
|
115
|
-
// Restore mocks to the values the rest of the suite expects.
|
|
116
|
-
mock.module("react-native", () => ({
|
|
117
|
-
Platform: {OS: "web"},
|
|
118
|
-
StyleSheet: {create: (s: unknown) => s},
|
|
119
|
-
}));
|
|
120
|
-
mock.module("./platform", () => ({IsWeb: true}));
|
|
121
|
-
mock.module("expo-secure-store", () => ({
|
|
122
|
-
deleteItemAsync: async () => {},
|
|
123
|
-
getItemAsync: async () => null,
|
|
124
|
-
setItemAsync: async () => {},
|
|
125
|
-
}));
|
|
126
|
-
});
|
|
127
|
-
|
|
128
114
|
it("stores tokens in SecureStore on native login", async () => {
|
|
129
115
|
const {store} = createTestStore();
|
|
130
116
|
store.dispatch({
|
package/src/betterAuthClient.ts
CHANGED
|
@@ -96,8 +96,7 @@ export const createBetterAuthClient = (config: BetterAuthClientConfig) => {
|
|
|
96
96
|
plugins: [
|
|
97
97
|
expoClient({
|
|
98
98
|
scheme: config.scheme,
|
|
99
|
-
|
|
100
|
-
storage: storage as any,
|
|
99
|
+
storage: storage as NonNullable<Parameters<typeof expoClient>[0]>["storage"],
|
|
101
100
|
storagePrefix: config.storagePrefix ?? "terreno",
|
|
102
101
|
}),
|
|
103
102
|
],
|
package/src/emptyApi.ts
CHANGED
|
@@ -207,8 +207,7 @@ export const getBaseQuery = (
|
|
|
207
207
|
return result;
|
|
208
208
|
}
|
|
209
209
|
},
|
|
210
|
-
|
|
211
|
-
})(args, api, extraOptions as any);
|
|
210
|
+
})(args, api, extraOptions as Parameters<ReturnType<typeof fetchBaseQuery>>[2]);
|
|
212
211
|
};
|
|
213
212
|
|
|
214
213
|
export const staggeredBaseQuery = retry(
|
|
@@ -355,8 +354,10 @@ export const emptySplitApi = createApi({
|
|
|
355
354
|
url: "/version-check",
|
|
356
355
|
}),
|
|
357
356
|
}),
|
|
358
|
-
|
|
359
|
-
|
|
357
|
+
...generateProfileEndpoints(
|
|
358
|
+
builder as unknown as Parameters<typeof generateProfileEndpoints>[0],
|
|
359
|
+
"users"
|
|
360
|
+
), // using 'users' here since it is highly intertwined with Users
|
|
360
361
|
}),
|
|
361
362
|
reducerPath: "terreno-rtk",
|
|
362
363
|
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process isolated coverage for constants.ts module-load debug paths.
|
|
3
|
+
*
|
|
4
|
+
* The base test suite loads constants.ts once with debug flags disabled, so the
|
|
5
|
+
* enable-time branches (AUTH_DEBUG / WEBSOCKETS_DEBUG log lines and the Expo
|
|
6
|
+
* tunnel warning) never execute under coverage. Here we re-mock expo-constants
|
|
7
|
+
* with every flag enabled and re-import the module through a cache-busting query
|
|
8
|
+
* so its top-level runs again while coverage instrumentation is active.
|
|
9
|
+
*/
|
|
10
|
+
import {beforeEach, describe, it, mock} from "bun:test";
|
|
11
|
+
import {assert} from "chai";
|
|
12
|
+
|
|
13
|
+
interface ConstantsModule {
|
|
14
|
+
AUTH_DEBUG: boolean;
|
|
15
|
+
logAuth: (...args: string[]) => void;
|
|
16
|
+
logSocket: (
|
|
17
|
+
user?: {featureFlags?: {debugWebsockets?: {enabled?: boolean}}} | boolean,
|
|
18
|
+
...args: string[]
|
|
19
|
+
) => void;
|
|
20
|
+
isWebsocketsDebugEnabled: () => boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const loadWithEnabledFlags = async (): Promise<{
|
|
24
|
+
module: ConstantsModule;
|
|
25
|
+
debugCalls: unknown[][];
|
|
26
|
+
errorCalls: unknown[][];
|
|
27
|
+
infoCalls: unknown[][];
|
|
28
|
+
}> => {
|
|
29
|
+
mock.module("expo-constants", () => ({
|
|
30
|
+
default: {
|
|
31
|
+
expoConfig: {extra: {AUTH_DEBUG: "true", WEBSOCKETS_DEBUG: "true"}},
|
|
32
|
+
expoGoConfig: {debuggerHost: "abc.exp.direct"},
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const debugCalls: unknown[][] = [];
|
|
37
|
+
const errorCalls: unknown[][] = [];
|
|
38
|
+
const infoCalls: unknown[][] = [];
|
|
39
|
+
const originalDebug = console.debug;
|
|
40
|
+
const originalError = console.error;
|
|
41
|
+
const originalInfo = console.info;
|
|
42
|
+
console.debug = (...args: unknown[]): void => {
|
|
43
|
+
debugCalls.push(args);
|
|
44
|
+
};
|
|
45
|
+
console.error = (...args: unknown[]): void => {
|
|
46
|
+
errorCalls.push(args);
|
|
47
|
+
};
|
|
48
|
+
console.info = (...args: unknown[]): void => {
|
|
49
|
+
infoCalls.push(args);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const constantsUrl = new URL("../constants.ts", import.meta.url).href;
|
|
54
|
+
const module = (await import(`${constantsUrl}?enabledFlags`)) as ConstantsModule;
|
|
55
|
+
return {debugCalls, errorCalls, infoCalls, module};
|
|
56
|
+
} finally {
|
|
57
|
+
console.debug = originalDebug;
|
|
58
|
+
console.error = originalError;
|
|
59
|
+
console.info = originalInfo;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe("constants module-load debug paths", () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
mock.restore();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("logs debug lines and the tunnel warning when every flag is enabled", async () => {
|
|
69
|
+
const {module, debugCalls, errorCalls} = await loadWithEnabledFlags();
|
|
70
|
+
|
|
71
|
+
assert.isTrue(module.AUTH_DEBUG);
|
|
72
|
+
assert.isTrue(module.isWebsocketsDebugEnabled());
|
|
73
|
+
assert.isTrue(
|
|
74
|
+
debugCalls.some((args) =>
|
|
75
|
+
args.some((value) => String(value).includes("AUTH_DEBUG is enabled"))
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
assert.isTrue(
|
|
79
|
+
debugCalls.some((args) =>
|
|
80
|
+
args.some((value) => String(value).includes("WEBSOCKETS_DEBUG is enabled"))
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
assert.isTrue(
|
|
84
|
+
errorCalls.some((args) =>
|
|
85
|
+
args.some((value) => String(value).includes("Expo Tunnel is not currently"))
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("routes logAuth and logSocket through console when flags are enabled", async () => {
|
|
91
|
+
const {module} = await loadWithEnabledFlags();
|
|
92
|
+
|
|
93
|
+
const debugCalls: unknown[][] = [];
|
|
94
|
+
const infoCalls: unknown[][] = [];
|
|
95
|
+
const originalDebug = console.debug;
|
|
96
|
+
const originalInfo = console.info;
|
|
97
|
+
console.debug = (...args: unknown[]): void => {
|
|
98
|
+
debugCalls.push(args);
|
|
99
|
+
};
|
|
100
|
+
console.info = (...args: unknown[]): void => {
|
|
101
|
+
infoCalls.push(args);
|
|
102
|
+
};
|
|
103
|
+
try {
|
|
104
|
+
module.logAuth("auth message");
|
|
105
|
+
module.logSocket(undefined, "socket message");
|
|
106
|
+
} finally {
|
|
107
|
+
console.debug = originalDebug;
|
|
108
|
+
console.info = originalInfo;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
assert.isTrue(
|
|
112
|
+
debugCalls.some((args) => args.some((value) => String(value).includes("auth message")))
|
|
113
|
+
);
|
|
114
|
+
assert.isTrue(
|
|
115
|
+
infoCalls.some((args) => args[0] === "[websocket]" && args[1] === "socket message")
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -89,8 +89,7 @@ const api = createApi({
|
|
|
89
89
|
|
|
90
90
|
const createTestStore = (endpoints = ["postTodos", "patchTodosById", "deleteTodosById"]) => {
|
|
91
91
|
const offline = createOfflineMiddleware({
|
|
92
|
-
|
|
93
|
-
api: api as any,
|
|
92
|
+
api: api as unknown as Parameters<typeof createOfflineMiddleware>[0]["api"],
|
|
94
93
|
endpoints,
|
|
95
94
|
});
|
|
96
95
|
|
|
@@ -45,8 +45,7 @@ const api = createApi({
|
|
|
45
45
|
|
|
46
46
|
const createNativeStore = () => {
|
|
47
47
|
const offline = createOfflineMiddleware({
|
|
48
|
-
|
|
49
|
-
api: api as any,
|
|
48
|
+
api: api as unknown as Parameters<typeof createOfflineMiddleware>[0]["api"],
|
|
50
49
|
endpoints: ["patchTodosById"],
|
|
51
50
|
});
|
|
52
51
|
|
|
@@ -195,6 +195,66 @@ describe("useUpgradeCheck (mobile)", () => {
|
|
|
195
195
|
});
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
describe("recheckOnForeground", () => {
|
|
199
|
+
it("re-checks when the app returns to the foreground", async () => {
|
|
200
|
+
const {result} = renderHook(() => useUpgradeCheck({recheckOnForeground: true}));
|
|
201
|
+
|
|
202
|
+
await act(async () => {
|
|
203
|
+
await flushPromises();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(result.current).toBeDefined();
|
|
207
|
+
expect(appStateListeners.length).toBeGreaterThan(0);
|
|
208
|
+
const initialCalls = mockTrigger.mock.calls.length;
|
|
209
|
+
|
|
210
|
+
await act(async () => {
|
|
211
|
+
// First go to the background, then return to the foreground: the
|
|
212
|
+
// background transition primes appState.current so the subsequent
|
|
213
|
+
// active transition satisfies the wasBackground && isNowActive guard.
|
|
214
|
+
for (const listener of appStateListeners) {
|
|
215
|
+
listener("background");
|
|
216
|
+
}
|
|
217
|
+
for (const listener of appStateListeners) {
|
|
218
|
+
listener("active");
|
|
219
|
+
}
|
|
220
|
+
await flushPromises();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(mockTrigger.mock.calls.length).toBeGreaterThan(initialCalls);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("does not re-check when the app stays inactive", async () => {
|
|
227
|
+
renderHook(() => useUpgradeCheck({recheckOnForeground: true}));
|
|
228
|
+
|
|
229
|
+
await act(async () => {
|
|
230
|
+
await flushPromises();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const initialCalls = mockTrigger.mock.calls.length;
|
|
234
|
+
|
|
235
|
+
await act(async () => {
|
|
236
|
+
for (const listener of appStateListeners) {
|
|
237
|
+
listener("inactive");
|
|
238
|
+
}
|
|
239
|
+
await flushPromises();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(mockTrigger.mock.calls.length).toBe(initialCalls);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("removes the AppState subscription on unmount", async () => {
|
|
246
|
+
const {unmount} = renderHook(() => useUpgradeCheck({recheckOnForeground: true}));
|
|
247
|
+
|
|
248
|
+
await act(async () => {
|
|
249
|
+
await flushPromises();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
mockRemove.mockClear();
|
|
253
|
+
unmount();
|
|
254
|
+
expect(mockRemove).toHaveBeenCalled();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
198
258
|
describe("canUpdate", () => {
|
|
199
259
|
it("is false on mobile when updateUrl is not set", () => {
|
|
200
260
|
const {result} = renderHook(() => useUpgradeCheck());
|
package/src/offlineGate.ts
CHANGED
|
@@ -13,8 +13,7 @@ export const configureOfflineMutationEndpoints = (endpoints: string[]): void =>
|
|
|
13
13
|
*/
|
|
14
14
|
export const shouldDeferOfflineMutation = (
|
|
15
15
|
endpointName: string,
|
|
16
|
-
|
|
17
|
-
getState: () => any
|
|
16
|
+
getState: () => unknown
|
|
18
17
|
): boolean => {
|
|
19
18
|
if (!offlineMutationEndpoints.has(endpointName)) {
|
|
20
19
|
return false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {describe, expect, it} from "bun:test";
|
|
1
|
+
import {describe, expect, it, mock} from "bun:test";
|
|
2
2
|
import {configureStore, type UnknownAction} from "@reduxjs/toolkit";
|
|
3
3
|
import type {Api} from "@reduxjs/toolkit/query/react";
|
|
4
4
|
|
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
} from "./offlineMiddleware";
|
|
11
11
|
import {type OfflineState, type QueuedMutation, selectOfflineQueue} from "./offlineSlice";
|
|
12
12
|
|
|
13
|
+
// Force IsWeb=true regardless of load order with the native test files.
|
|
14
|
+
mock.module("./platform", () => ({IsWeb: true}));
|
|
15
|
+
|
|
13
16
|
interface QueryEntry {
|
|
14
17
|
data?: {data: Record<string, unknown>[]};
|
|
15
18
|
originalArgs: unknown;
|
|
@@ -47,6 +50,7 @@ const initialTestApiState: TestApiState = {
|
|
|
47
50
|
|
|
48
51
|
const testApiReducer = (state: TestApiState = initialTestApiState): TestApiState => state;
|
|
49
52
|
|
|
53
|
+
// noExplicitAny: Test double covers only fields used by offline middleware.
|
|
50
54
|
// biome-ignore lint/suspicious/noExplicitAny: Test double covers only fields used by offline middleware.
|
|
51
55
|
const createTestApi = (): Api<any, any, any, any> => {
|
|
52
56
|
return {
|
|
@@ -66,6 +70,7 @@ const createTestApi = (): Api<any, any, any, any> => {
|
|
|
66
70
|
invalidateTags: () => ({type: "testApi/invalidateTags"}),
|
|
67
71
|
updateQueryData: () => ({type: "testApi/updateQueryData"}),
|
|
68
72
|
},
|
|
73
|
+
// noExplicitAny: Test double covers only fields used by offline middleware.
|
|
69
74
|
// biome-ignore lint/suspicious/noExplicitAny: Test double covers only fields used by offline middleware.
|
|
70
75
|
} as unknown as Api<any, any, any, any>;
|
|
71
76
|
};
|
package/src/offlineMiddleware.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface OfflineMiddlewareConfig {
|
|
|
28
28
|
/** RTK Query mutation endpoint names to queue when offline */
|
|
29
29
|
endpoints: string[];
|
|
30
30
|
/** The RTK Query API instance */
|
|
31
|
+
// noExplicitAny: Generic API type
|
|
31
32
|
// biome-ignore lint/suspicious/noExplicitAny: Generic API type
|
|
32
33
|
api: Api<any, any, any, any>;
|
|
33
34
|
}
|
|
@@ -54,6 +55,7 @@ const inferMutationType = (endpointName: string): "create" | "update" | "delete"
|
|
|
54
55
|
* not an auth or application-level FETCH_ERROR from emptyApi.
|
|
55
56
|
*/
|
|
56
57
|
export const isNetworkFetchError = (
|
|
58
|
+
// noExplicitAny: RTK Query error shapes vary by source
|
|
57
59
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query error shapes vary by source
|
|
58
60
|
source: any
|
|
59
61
|
): boolean => {
|
|
@@ -141,8 +143,10 @@ const inferGetByIdEndpointName = (endpointName: string): string => {
|
|
|
141
143
|
const extractBaseUpdatedAt = (
|
|
142
144
|
endpointName: string,
|
|
143
145
|
originalArgs: unknown,
|
|
146
|
+
// noExplicitAny: Generic getState
|
|
144
147
|
// biome-ignore lint/suspicious/noExplicitAny: Generic getState
|
|
145
148
|
getState: () => any,
|
|
149
|
+
// noExplicitAny: Generic API type
|
|
146
150
|
// biome-ignore lint/suspicious/noExplicitAny: Generic API type
|
|
147
151
|
api: Api<any, any, any, any>
|
|
148
152
|
): string | undefined => {
|
|
@@ -162,6 +166,7 @@ const extractBaseUpdatedAt = (
|
|
|
162
166
|
|
|
163
167
|
const getByIdEndpoint = inferGetByIdEndpointName(endpointName);
|
|
164
168
|
const state = getState();
|
|
169
|
+
// noExplicitAny: RTK Query internal state shape
|
|
165
170
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query internal state shape
|
|
166
171
|
const queries: Record<string, any> = state[api.reducerPath]?.queries ?? {};
|
|
167
172
|
for (const key of Object.keys(queries)) {
|
|
@@ -188,13 +193,16 @@ const extractBaseUpdatedAt = (
|
|
|
188
193
|
* args the consumer passed to the query hook, e.g. `{}` vs `undefined`).
|
|
189
194
|
*/
|
|
190
195
|
const getCachedQueryArgs = (
|
|
196
|
+
// noExplicitAny: Generic API state shape
|
|
191
197
|
// biome-ignore lint/suspicious/noExplicitAny: Generic API state shape
|
|
192
198
|
getState: () => any,
|
|
199
|
+
// noExplicitAny: Generic API type
|
|
193
200
|
// biome-ignore lint/suspicious/noExplicitAny: Generic API type
|
|
194
201
|
api: Api<any, any, any, any>,
|
|
195
202
|
listEndpointName: string
|
|
196
203
|
): unknown[] => {
|
|
197
204
|
const state = getState();
|
|
205
|
+
// noExplicitAny: RTK Query internal state shape
|
|
198
206
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query internal state shape
|
|
199
207
|
const queries: Record<string, any> = state[api.reducerPath]?.queries ?? {};
|
|
200
208
|
const cachedArgs: unknown[] = [];
|
|
@@ -213,10 +221,13 @@ const getCachedQueryArgs = (
|
|
|
213
221
|
* Apply an optimistic update to the RTK Query cache for a queued mutation.
|
|
214
222
|
*/
|
|
215
223
|
const applyOptimisticUpdate = (
|
|
224
|
+
// noExplicitAny: Generic API type
|
|
216
225
|
// biome-ignore lint/suspicious/noExplicitAny: Generic API type
|
|
217
226
|
api: Api<any, any, any, any>,
|
|
227
|
+
// noExplicitAny: Generic dispatch
|
|
218
228
|
// biome-ignore lint/suspicious/noExplicitAny: Generic dispatch
|
|
219
229
|
dispatch: any,
|
|
230
|
+
// noExplicitAny: Generic getState
|
|
220
231
|
// biome-ignore lint/suspicious/noExplicitAny: Generic getState
|
|
221
232
|
getState: () => any,
|
|
222
233
|
mutation: QueuedMutation
|
|
@@ -225,10 +236,12 @@ const applyOptimisticUpdate = (
|
|
|
225
236
|
const listEndpointName = `get${tagType.charAt(0).toUpperCase() + tagType.slice(1)}`;
|
|
226
237
|
const cachedArgs = getCachedQueryArgs(getState, api, listEndpointName);
|
|
227
238
|
|
|
239
|
+
// noExplicitAny: RTK Query cache shape varies by endpoint
|
|
228
240
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query cache shape varies by endpoint
|
|
229
241
|
const updateAllCacheEntries = (updater: (draft: any) => void): void => {
|
|
230
242
|
for (const queryArg of cachedArgs) {
|
|
231
243
|
dispatch(
|
|
244
|
+
// noExplicitAny: RTK Query cache shape varies by endpoint
|
|
232
245
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query cache shape varies by endpoint
|
|
233
246
|
api.util.updateQueryData(listEndpointName as any, queryArg, updater)
|
|
234
247
|
);
|
|
@@ -238,6 +251,7 @@ const applyOptimisticUpdate = (
|
|
|
238
251
|
if (mutation.type === "create") {
|
|
239
252
|
const args = mutation.args as {body?: Record<string, unknown>};
|
|
240
253
|
const tempItem = buildOptimisticCreateItem(mutation, args?.body);
|
|
254
|
+
// noExplicitAny: RTK Query cache shape varies by endpoint
|
|
241
255
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query cache shape varies by endpoint
|
|
242
256
|
updateAllCacheEntries((draft: any) => {
|
|
243
257
|
if (draft?.data && Array.isArray(draft.data)) {
|
|
@@ -247,6 +261,7 @@ const applyOptimisticUpdate = (
|
|
|
247
261
|
} else if (mutation.type === "update") {
|
|
248
262
|
const args = mutation.args as {id?: string; body?: Record<string, unknown>};
|
|
249
263
|
if (args?.id && args?.body) {
|
|
264
|
+
// noExplicitAny: RTK Query cache shape varies by endpoint
|
|
250
265
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query cache shape varies by endpoint
|
|
251
266
|
updateAllCacheEntries((draft: any) => {
|
|
252
267
|
if (draft?.data && Array.isArray(draft.data)) {
|
|
@@ -262,6 +277,7 @@ const applyOptimisticUpdate = (
|
|
|
262
277
|
} else if (mutation.type === "delete") {
|
|
263
278
|
const args = mutation.args as {id?: string};
|
|
264
279
|
if (args?.id) {
|
|
280
|
+
// noExplicitAny: RTK Query cache shape varies by endpoint
|
|
265
281
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query cache shape varies by endpoint
|
|
266
282
|
updateAllCacheEntries((draft: any) => {
|
|
267
283
|
if (draft?.data && Array.isArray(draft.data)) {
|
|
@@ -278,10 +294,13 @@ const applyOptimisticUpdate = (
|
|
|
278
294
|
* Remove optimistic temp items from the cache after they've been replayed.
|
|
279
295
|
*/
|
|
280
296
|
const removeTempItems = (
|
|
297
|
+
// noExplicitAny: Generic API type
|
|
281
298
|
// biome-ignore lint/suspicious/noExplicitAny: Generic API type
|
|
282
299
|
api: Api<any, any, any, any>,
|
|
300
|
+
// noExplicitAny: Generic dispatch
|
|
283
301
|
// biome-ignore lint/suspicious/noExplicitAny: Generic dispatch
|
|
284
302
|
dispatch: any,
|
|
303
|
+
// noExplicitAny: Generic getState
|
|
285
304
|
// biome-ignore lint/suspicious/noExplicitAny: Generic getState
|
|
286
305
|
getState: () => any,
|
|
287
306
|
mutations: QueuedMutation[]
|
|
@@ -295,6 +314,7 @@ const removeTempItems = (
|
|
|
295
314
|
|
|
296
315
|
for (const queryArg of cachedArgs) {
|
|
297
316
|
dispatch(
|
|
317
|
+
// noExplicitAny: RTK Query cache shape varies by endpoint
|
|
298
318
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query cache shape varies by endpoint
|
|
299
319
|
api.util.updateQueryData(listEndpointName as any, queryArg, (draft: any) => {
|
|
300
320
|
if (draft?.data && Array.isArray(draft.data)) {
|
|
@@ -369,6 +389,7 @@ const replayMutation = async (
|
|
|
369
389
|
* Web apps should use `useServerStatus` for server-level health checking.
|
|
370
390
|
*/
|
|
371
391
|
const setupNativeNetworkMonitoring = (
|
|
392
|
+
// noExplicitAny: Generic dispatch
|
|
372
393
|
// biome-ignore lint/suspicious/noExplicitAny: Generic dispatch
|
|
373
394
|
dispatch: any
|
|
374
395
|
): (() => void) => {
|
|
@@ -438,6 +459,7 @@ export const createOfflineMiddleware = (
|
|
|
438
459
|
// RTK Query dispatches actions with type "terreno-rtk/executeMutation/rejected"
|
|
439
460
|
// when a mutation fails. Queue the mutation and apply an optimistic cache update.
|
|
440
461
|
listenerMiddleware.startListening({
|
|
462
|
+
// noExplicitAny: RTK Query action types are complex
|
|
441
463
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query action types are complex
|
|
442
464
|
effect: async (action: any, listenerApi) => {
|
|
443
465
|
const state = listenerApi.getState() as {offline: OfflineState};
|
|
@@ -464,11 +486,13 @@ export const createOfflineMiddleware = (
|
|
|
464
486
|
const listEndpointName = `get${tagType.charAt(0).toUpperCase() + tagType.slice(1)}`;
|
|
465
487
|
const cachedArgs = getCachedQueryArgs(listenerApi.getState, api, listEndpointName);
|
|
466
488
|
for (const queryArg of cachedArgs) {
|
|
489
|
+
// noExplicitAny: RTK Query cache shape and endpoint types vary
|
|
467
490
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query cache shape and endpoint types vary
|
|
468
491
|
const endpoint = (api.endpoints as any)[listEndpointName];
|
|
469
492
|
if (!endpoint?.select) {
|
|
470
493
|
continue;
|
|
471
494
|
}
|
|
495
|
+
// noExplicitAny: RTK Query cache shape varies
|
|
472
496
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query cache shape varies
|
|
473
497
|
const cacheEntry = endpoint.select(queryArg)(listenerApi.getState() as any) as any;
|
|
474
498
|
const items = cacheEntry?.data?.data;
|
|
@@ -513,6 +537,7 @@ export const createOfflineMiddleware = (
|
|
|
513
537
|
listenerApi.dispatch(setOnlineStatus(false));
|
|
514
538
|
}
|
|
515
539
|
},
|
|
540
|
+
// noExplicitAny: RTK Query internal action shape
|
|
516
541
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query internal action shape
|
|
517
542
|
predicate: (action: any) => {
|
|
518
543
|
if (typeof action?.type !== "string") {
|
|
@@ -535,6 +560,7 @@ export const createOfflineMiddleware = (
|
|
|
535
560
|
listenerApi.dispatch(setOnlineStatus(false));
|
|
536
561
|
}
|
|
537
562
|
},
|
|
563
|
+
// noExplicitAny: RTK Query internal action shape
|
|
538
564
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query internal action shape
|
|
539
565
|
predicate: (action: any) => {
|
|
540
566
|
if (typeof action?.type !== "string") {
|
|
@@ -557,6 +583,7 @@ export const createOfflineMiddleware = (
|
|
|
557
583
|
listenerApi.dispatch(setOnlineStatus(true));
|
|
558
584
|
}
|
|
559
585
|
},
|
|
586
|
+
// noExplicitAny: RTK Query internal action shape
|
|
560
587
|
// biome-ignore lint/suspicious/noExplicitAny: RTK Query internal action shape
|
|
561
588
|
predicate: (action: any) => {
|
|
562
589
|
if (typeof action?.type !== "string") {
|
package/src/realtime.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// noExplicitAny: realtime RTK tests mock Socket.io and RTK Query runtime shapes
|
|
1
2
|
// biome-ignore-all lint/suspicious/noExplicitAny: realtime RTK tests mock Socket.io and RTK Query runtime shapes
|
|
2
3
|
import {afterEach, describe, expect, it} from "bun:test";
|
|
3
4
|
import type {Socket} from "socket.io-client";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {afterEach, describe, expect, it, mock} from "bun:test";
|
|
2
|
+
import {configureStore} from "@reduxjs/toolkit";
|
|
3
|
+
|
|
4
|
+
import {createSessionApi} from "./sessionApi";
|
|
5
|
+
|
|
6
|
+
type FetchArgs = Parameters<typeof fetch>;
|
|
7
|
+
|
|
8
|
+
interface RecordedFetch {
|
|
9
|
+
urls: string[];
|
|
10
|
+
handler: (input: FetchArgs[0], init: FetchArgs[1]) => Response;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const jsonResponse = (body: unknown, status = 200): Response =>
|
|
14
|
+
new Response(JSON.stringify(body), {
|
|
15
|
+
headers: {"Content-Type": "application/json"},
|
|
16
|
+
status,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const originalFetch = global.fetch;
|
|
20
|
+
let recorded: RecordedFetch;
|
|
21
|
+
|
|
22
|
+
const setFetch = (handler: RecordedFetch["handler"]): void => {
|
|
23
|
+
recorded = {handler, urls: []};
|
|
24
|
+
global.fetch = mock((input: FetchArgs[0], init: FetchArgs[1]) => {
|
|
25
|
+
const url =
|
|
26
|
+
typeof input === "string" ? input : input instanceof Request ? input.url : String(input);
|
|
27
|
+
recorded.urls.push(url);
|
|
28
|
+
return Promise.resolve(handler(input, init));
|
|
29
|
+
}) as unknown as typeof fetch;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const buildStore = () => {
|
|
33
|
+
const api = createSessionApi({baseUrl: "http://localhost:9999"});
|
|
34
|
+
const withEndpoints = api.injectEndpoints({
|
|
35
|
+
endpoints: (build) => ({
|
|
36
|
+
getThing: build.query<unknown, void>({query: () => "/thing"}),
|
|
37
|
+
getWithParams: build.query<unknown, void>({
|
|
38
|
+
query: () => ({params: {ids: {$in: [1, 2]}, status: "active"}, url: "/things"}),
|
|
39
|
+
}),
|
|
40
|
+
}),
|
|
41
|
+
overrideExisting: true,
|
|
42
|
+
});
|
|
43
|
+
const store = configureStore({
|
|
44
|
+
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware),
|
|
45
|
+
reducer: {[api.reducerPath]: api.reducer},
|
|
46
|
+
});
|
|
47
|
+
return {api, store, withEndpoints};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
describe("createSessionApi", () => {
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
global.fetch = originalFetch;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("defaults reducerPath to terreno-session", () => {
|
|
56
|
+
const api = createSessionApi();
|
|
57
|
+
expect(api.reducerPath).toBe("terreno-session");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("uses a custom reducerPath when provided", () => {
|
|
61
|
+
const api = createSessionApi({reducerPath: "admin-session"});
|
|
62
|
+
expect(api.reducerPath).toBe("admin-session");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("responseHandler", () => {
|
|
66
|
+
it("returns null for a 204 No Content response", async () => {
|
|
67
|
+
setFetch(() => new Response(null, {status: 204}));
|
|
68
|
+
const {store, withEndpoints} = buildStore();
|
|
69
|
+
|
|
70
|
+
const result = await store.dispatch(withEndpoints.endpoints.getThing.initiate());
|
|
71
|
+
|
|
72
|
+
expect(result.error).toBeUndefined();
|
|
73
|
+
expect(result.data).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns the whole payload for list responses containing `more`", async () => {
|
|
77
|
+
const payload = {data: [{id: "1"}], more: false, page: 1};
|
|
78
|
+
setFetch(() => jsonResponse(payload));
|
|
79
|
+
const {store, withEndpoints} = buildStore();
|
|
80
|
+
|
|
81
|
+
const result = await store.dispatch(withEndpoints.endpoints.getThing.initiate());
|
|
82
|
+
|
|
83
|
+
expect(result.data).toEqual(payload);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("unwraps the `data` envelope for single-resource responses", async () => {
|
|
87
|
+
setFetch(() => jsonResponse({data: {id: "42", name: "answer"}}));
|
|
88
|
+
const {store, withEndpoints} = buildStore();
|
|
89
|
+
|
|
90
|
+
const result = await store.dispatch(withEndpoints.endpoints.getThing.initiate());
|
|
91
|
+
|
|
92
|
+
expect(result.data).toEqual({id: "42", name: "answer"});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns the raw result when there is no `data` envelope or `more` field", async () => {
|
|
96
|
+
setFetch(() => jsonResponse({id: "7", name: "raw"}));
|
|
97
|
+
const {store, withEndpoints} = buildStore();
|
|
98
|
+
|
|
99
|
+
const result = await store.dispatch(withEndpoints.endpoints.getThing.initiate());
|
|
100
|
+
|
|
101
|
+
expect(result.data).toEqual({id: "7", name: "raw"});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("serializes nested query params with qs", async () => {
|
|
106
|
+
setFetch(() => jsonResponse({id: "1"}));
|
|
107
|
+
const {store, withEndpoints} = buildStore();
|
|
108
|
+
|
|
109
|
+
await store.dispatch(withEndpoints.endpoints.getWithParams.initiate());
|
|
110
|
+
|
|
111
|
+
const requestedUrl = recorded.urls[0];
|
|
112
|
+
expect(requestedUrl).toContain("ids%5B%24in%5D%5B0%5D=1");
|
|
113
|
+
expect(requestedUrl).toContain("status=active");
|
|
114
|
+
});
|
|
115
|
+
});
|
package/src/sync.test.ts
CHANGED
|
@@ -87,8 +87,7 @@ const createApi = (queries: Record<string, QueryEntry> | null) => {
|
|
|
87
87
|
|
|
88
88
|
const renderSync = (
|
|
89
89
|
socket: FakeSocket | null,
|
|
90
|
-
|
|
91
|
-
api: any,
|
|
90
|
+
api: ReturnType<typeof createApi>["api"],
|
|
92
91
|
store: ReturnType<typeof configureStore>,
|
|
93
92
|
options: {tagTypes?: string[]; debug?: boolean} = {}
|
|
94
93
|
) => {
|
package/src/sync.ts
CHANGED
|
@@ -40,7 +40,8 @@ interface UseSyncConnectionOptions {
|
|
|
40
40
|
/** Socket.io client instance (from useSocketConnection) */
|
|
41
41
|
socket: Socket | null;
|
|
42
42
|
/** RTK Query API instance (the enhanced API with tag types) */
|
|
43
|
-
//
|
|
43
|
+
// noExplicitAny: RTK Query Api generic requires four app-specific type parameters
|
|
44
|
+
// biome-ignore lint/suspicious/noExplicitAny: RTK Query Api generic requires four app-specific type parameters
|
|
44
45
|
api: any;
|
|
45
46
|
/** Tag types to listen for (e.g. ["todos", "users"]) — these should match the collection field in events */
|
|
46
47
|
tagTypes: string[];
|
package/src/testPreload.test.ts
CHANGED
|
@@ -23,6 +23,13 @@ describe("test-preload default mocks", () => {
|
|
|
23
23
|
expect(state.isConnected).toBe(true);
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
it("expo-network mock exposes addNetworkStateListener", async () => {
|
|
27
|
+
const network = await import("expo-network");
|
|
28
|
+
const subscription = network.addNetworkStateListener(() => {});
|
|
29
|
+
expect(subscription.remove).toBeTypeOf("function");
|
|
30
|
+
subscription.remove();
|
|
31
|
+
});
|
|
32
|
+
|
|
26
33
|
it("expo-constants mock exposes an empty config", async () => {
|
|
27
34
|
const Constants = (await import("expo-constants")).default;
|
|
28
35
|
expect(Constants.expoConfig?.extra).toBeDefined();
|