@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.
Files changed (77) hide show
  1. package/dist/authSlice.d.ts.map +1 -1
  2. package/dist/authSlice.js +2 -0
  3. package/dist/authSlice.js.map +1 -1
  4. package/dist/authSlice.test.js +4 -1
  5. package/dist/authSlice.test.js.map +1 -1
  6. package/dist/authSliceNative.test.js +3 -16
  7. package/dist/authSliceNative.test.js.map +1 -1
  8. package/dist/betterAuthClient.d.ts.map +1 -1
  9. package/dist/betterAuthClient.js +0 -1
  10. package/dist/betterAuthClient.js.map +1 -1
  11. package/dist/betterAuthSlice.test.js +1 -0
  12. package/dist/betterAuthSlice.test.js.map +1 -1
  13. package/dist/emptyApi.d.ts.map +1 -1
  14. package/dist/emptyApi.js +0 -2
  15. package/dist/emptyApi.js.map +1 -1
  16. package/dist/isolated/constants.isolated.d.ts +2 -0
  17. package/dist/isolated/constants.isolated.d.ts.map +1 -0
  18. package/dist/isolated/constants.isolated.js +81 -0
  19. package/dist/isolated/constants.isolated.js.map +1 -0
  20. package/dist/isolated/offlineMiddleware.isolated.js +0 -1
  21. package/dist/isolated/offlineMiddleware.isolated.js.map +1 -1
  22. package/dist/isolated/offlineMiddleware.native.isolated.js +0 -1
  23. package/dist/isolated/offlineMiddleware.native.isolated.js.map +1 -1
  24. package/dist/isolated/useUpgradeCheck.mobile.isolated.js +47 -0
  25. package/dist/isolated/useUpgradeCheck.mobile.isolated.js.map +1 -1
  26. package/dist/offlineGate.d.ts +1 -1
  27. package/dist/offlineGate.d.ts.map +1 -1
  28. package/dist/offlineGate.js +1 -3
  29. package/dist/offlineGate.js.map +1 -1
  30. package/dist/offlineMiddleware.d.ts.map +1 -1
  31. package/dist/offlineMiddleware.js +26 -0
  32. package/dist/offlineMiddleware.js.map +1 -1
  33. package/dist/offlineMiddleware.test.js +5 -1
  34. package/dist/offlineMiddleware.test.js.map +1 -1
  35. package/dist/realtime.test.js +1 -0
  36. package/dist/realtime.test.js.map +1 -1
  37. package/dist/sessionApi.test.d.ts +2 -0
  38. package/dist/sessionApi.test.d.ts.map +1 -0
  39. package/dist/sessionApi.test.js +84 -0
  40. package/dist/sessionApi.test.js.map +1 -0
  41. package/dist/sync.d.ts.map +1 -1
  42. package/dist/sync.js.map +1 -1
  43. package/dist/sync.test.js +1 -3
  44. package/dist/sync.test.js.map +1 -1
  45. package/dist/testPreload.test.js +6 -0
  46. package/dist/testPreload.test.js.map +1 -1
  47. package/dist/useFeatureFlags.d.ts.map +1 -1
  48. package/dist/useFeatureFlags.js.map +1 -1
  49. package/dist/useTerrenoFeatureFlags.d.ts.map +1 -1
  50. package/dist/useTerrenoFeatureFlags.js.map +1 -1
  51. package/dist/useTerrenoFeatureFlags.test.js +10 -0
  52. package/dist/useTerrenoFeatureFlags.test.js.map +1 -1
  53. package/dist/useUpgradeCheckNative.test.js +21 -0
  54. package/dist/useUpgradeCheckNative.test.js.map +1 -1
  55. package/package.json +2 -2
  56. package/src/authSlice.test.ts +5 -1
  57. package/src/authSlice.ts +2 -0
  58. package/src/authSliceNative.test.ts +4 -18
  59. package/src/betterAuthClient.ts +1 -2
  60. package/src/betterAuthSlice.test.ts +1 -0
  61. package/src/emptyApi.ts +5 -4
  62. package/src/isolated/constants.isolated.ts +118 -0
  63. package/src/isolated/offlineMiddleware.isolated.ts +1 -2
  64. package/src/isolated/offlineMiddleware.native.isolated.ts +1 -2
  65. package/src/isolated/useUpgradeCheck.mobile.isolated.ts +60 -0
  66. package/src/offlineGate.ts +1 -2
  67. package/src/offlineMiddleware.test.ts +6 -1
  68. package/src/offlineMiddleware.ts +27 -0
  69. package/src/realtime.test.ts +1 -0
  70. package/src/sessionApi.test.ts +115 -0
  71. package/src/sync.test.ts +1 -2
  72. package/src/sync.ts +2 -1
  73. package/src/testPreload.test.ts +7 -0
  74. package/src/useFeatureFlags.ts +1 -0
  75. package/src/useTerrenoFeatureFlags.test.ts +16 -0
  76. package/src/useTerrenoFeatureFlags.ts +2 -0
  77. package/src/useUpgradeCheckNative.test.ts +26 -0
@@ -1,5 +1,4 @@
1
- // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
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 as any);
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({
@@ -96,8 +96,7 @@ export const createBetterAuthClient = (config: BetterAuthClientConfig) => {
96
96
  plugins: [
97
97
  expoClient({
98
98
  scheme: config.scheme,
99
- // biome-ignore lint/suspicious/noExplicitAny: Better Auth storage type is flexible
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
  ],
@@ -1,3 +1,4 @@
1
+ // noExplicitAny: test mock typing
1
2
  // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
2
3
  import {beforeEach, describe, expect, it, mock} from "bun:test";
3
4
  import {configureStore} from "@reduxjs/toolkit";
package/src/emptyApi.ts CHANGED
@@ -207,8 +207,7 @@ export const getBaseQuery = (
207
207
  return result;
208
208
  }
209
209
  },
210
- // biome-ignore lint/suspicious/noExplicitAny: Weird typing from rtk query
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
- // biome-ignore lint/suspicious/noExplicitAny: Generic
359
- ...generateProfileEndpoints(builder as any, "users"), // using 'users' here since it is highly intertwined with Users
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
- // biome-ignore lint/suspicious/noExplicitAny: Generic API type is intentionally broad.
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
- // biome-ignore lint/suspicious/noExplicitAny: Generic API type is intentionally broad.
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());
@@ -13,8 +13,7 @@ export const configureOfflineMutationEndpoints = (endpoints: string[]): void =>
13
13
  */
14
14
  export const shouldDeferOfflineMutation = (
15
15
  endpointName: string,
16
- // biome-ignore lint/suspicious/noExplicitAny: Redux getState is app-specific
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
  };
@@ -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") {
@@ -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
- // biome-ignore lint/suspicious/noExplicitAny: test-only structural RTK Query api stub
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
- // biome-ignore lint/suspicious/noExplicitAny: noExplicitAny: RTK Query Api generic requires four app-specific type parameters
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[];
@@ -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();