Skip to content

Commit 8150df2

Browse files
authored
Merge pull request #118 from simonsobs/dev
Clean code in `@/api/`, add tests
2 parents 43dcded + d7af645 commit 8150df2

7 files changed

+281
-32
lines changed

src/api/__tests__/mock-use-subscription-response.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ vi.mock("@urql/vue", () => ({
1010
useSubscription: vi.fn(),
1111
}));
1212

13-
export const query = gql`
13+
const query = gql`
1414
subscription CtrlStateS {
1515
ctrlState
1616
}
1717
`;
1818

19-
export type Data = { ctrlState: string };
19+
type Data = { ctrlState: string };
2020

2121
const fcState = fc.string({ minLength: 1 });
2222
const fcData = fc.oneof(fc.constant(undefined), fc.record({ ctrlState: fcState }));
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, expect, it } from "vitest";
2+
import fc from "fast-check";
3+
4+
import { useMappedWithFallback } from "../use-mapped-with-fallback";
5+
6+
import { mockUseQueryResponse } from "./mock-use-query-response";
7+
import { mockUseSubscriptionResponse } from "./mock-use-subscription-response";
8+
9+
type QueryData = { ctrl: { state: string } };
10+
type SubscriptionData = { ctrlState: string };
11+
12+
const fcState = fc.string();
13+
const fcQueryData = fc.oneof(
14+
fc.constant(undefined),
15+
fc.record({ ctrl: fc.record({ state: fcState }) }),
16+
);
17+
const fcSubData = fc.oneof(fc.constant(undefined), fc.record({ ctrlState: fcState }));
18+
19+
const fcErrorInstance = fc.string().map((msg) => new Error(msg));
20+
const fcError = fc.oneof(fc.constant(undefined), fcErrorInstance);
21+
22+
const fcQueryArg = fc.record({ data: fcQueryData, error: fcError });
23+
const fcSubArg = fc.record({ data: fcSubData, error: fcError });
24+
25+
describe("useMappedWithFallback()", () => {
26+
it("Property test", async () => {
27+
await fc.assert(
28+
fc.asyncProperty(fcQueryArg, fc.array(fcSubArg), async (queryArg, subArg) => {
29+
const { response: response1, issue } =
30+
mockUseSubscriptionResponse<SubscriptionData>(subArg);
31+
const response2 = await mockUseQueryResponse<QueryData>(queryArg);
32+
33+
const map1 = (d: typeof response1.data) => d.value?.ctrlState;
34+
const map2 = (d: typeof response2.data) => d.value?.ctrl.state;
35+
36+
const options = { response1, response2, map1, map2 };
37+
const { data, error } = useMappedWithFallback(options);
38+
39+
// Assert initial values are from query.
40+
expect(error.value).toBe(queryArg.error);
41+
expect(data.value).toBe(queryArg.error ? undefined : queryArg.data?.ctrl.state);
42+
43+
// Assert the subsequent values are issued from subscription backed up by query.
44+
for (const issued of issue) {
45+
const expectedError = issued.error ?? queryArg.error;
46+
const expectedState = expectedError
47+
? undefined
48+
: (issued.data?.ctrlState ?? queryArg.data?.ctrl.state);
49+
expect(error.value).toBe(expectedError);
50+
expect(data.value).toBe(expectedState);
51+
}
52+
}),
53+
);
54+
});
55+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import fc from "fast-check";
3+
4+
import type {
5+
CtrlRunNoQuery,
6+
CtrlRunNoSSubscription,
7+
} from "@/graphql/codegen/generated";
8+
import {
9+
useCtrlRunNoQuery,
10+
useCtrlRunNoSSubscription,
11+
} from "@/graphql/codegen/generated";
12+
13+
import { useSubscribeRunNo } from "../use-run-no-subscription";
14+
15+
import { mockUseQueryResponse } from "./mock-use-query-response";
16+
import { mockUseSubscriptionResponse } from "./mock-use-subscription-response";
17+
18+
vi.mock("@/graphql/codegen/generated", () => ({
19+
useCtrlRunNoQuery: vi.fn(),
20+
useCtrlRunNoSSubscription: vi.fn(),
21+
}));
22+
23+
const fcRunNo = fc.integer();
24+
const fcQueryData = fc.oneof(
25+
fc.constant(undefined),
26+
fc.record({ ctrl: fc.record({ runNo: fcRunNo }) }),
27+
);
28+
const fcSubData = fc.oneof(fc.constant(undefined), fc.record({ ctrlRunNo: fcRunNo }));
29+
30+
const fcErrorInstance = fc.string().map((msg) => new Error(msg));
31+
const fcError = fc.oneof(fc.constant(undefined), fcErrorInstance);
32+
33+
const fcQueryArg = fc.record({ data: fcQueryData, error: fcError });
34+
const fcSubArg = fc.record({ data: fcSubData, error: fcError });
35+
36+
describe("useSubscribeRunNo()", () => {
37+
beforeEach(() => {
38+
vi.clearAllMocks();
39+
});
40+
41+
afterEach(() => {
42+
vi.resetAllMocks();
43+
});
44+
45+
it("Property test", async () => {
46+
await fc.assert(
47+
fc.asyncProperty(fcQueryArg, fc.array(fcSubArg), async (queryArg, subArg) => {
48+
// Mock useCtrlRunNoQuery()
49+
const queryRes = mockUseQueryResponse<CtrlRunNoQuery>(queryArg);
50+
type Query = ReturnType<typeof useCtrlRunNoQuery>;
51+
vi.mocked(useCtrlRunNoQuery).mockReturnValue(queryRes as Query);
52+
53+
// Mock useCtrlRunNoSSubscription()
54+
const { response: subRes, issue } =
55+
mockUseSubscriptionResponse<CtrlRunNoSSubscription>(subArg);
56+
type SubRes = ReturnType<typeof useCtrlRunNoSSubscription>;
57+
vi.mocked(useCtrlRunNoSSubscription).mockReturnValue(subRes as SubRes);
58+
59+
const { runNo, error } = await useSubscribeRunNo();
60+
61+
// Assert initial values are from query.
62+
expect(error.value).toBe(queryArg.error);
63+
expect(runNo.value).toBe(
64+
queryArg.error ? undefined : queryArg.data?.ctrl.runNo,
65+
);
66+
67+
// Assert the subsequent values are issued from subscription backed up by query.
68+
for (const issued of issue) {
69+
const expectedError = issued.error || queryArg.error;
70+
const expectedRunNo = expectedError
71+
? undefined
72+
: (issued.data?.ctrlRunNo ?? queryArg.data?.ctrl.runNo);
73+
expect(error.value).toBe(expectedError);
74+
expect(runNo.value).toBe(expectedRunNo);
75+
}
76+
}),
77+
);
78+
});
79+
});

src/api/__tests__/use-state-subscription.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ vi.mock("@/graphql/codegen/generated", () => ({
2020
useCtrlStateSSubscription: vi.fn(),
2121
}));
2222

23-
const fcState = fc.string({ minLength: 1 });
23+
const fcState = fc.string();
2424
const fcQueryData = fc.oneof(
2525
fc.constant(undefined),
2626
fc.record({ ctrl: fc.record({ state: fcState }) }),
@@ -69,7 +69,7 @@ describe("useSubscribeState()", () => {
6969
const expectedError = issued.error || queryArg.error;
7070
const expectedState = expectedError
7171
? undefined
72-
: issued.data?.ctrlState || queryArg.data?.ctrl.state;
72+
: (issued.data?.ctrlState ?? queryArg.data?.ctrl.state);
7373
expect(error.value).toBe(expectedError);
7474
expect(state.value).toBe(expectedState);
7575
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { computed } from "vue";
2+
import type { ComputedRef, Ref } from "vue";
3+
4+
/**
5+
* Response returned by `useQuery()` / `useSubscription()`.
6+
*
7+
* `T` is the data type, the equivalent to the first argument of `UseQueryResponse` and
8+
* `UseSubscriptionResponse`.
9+
*/
10+
interface Response<T> {
11+
data: Ref<T | undefined>;
12+
error: Ref<Error | undefined>;
13+
}
14+
15+
/**
16+
* Options for the `useMappedWithFallback` composable.
17+
*
18+
* The interface includes two responses and two mapping functions. The two responses are
19+
* primary and secondary, which are typically returned by `useSubscription()` and
20+
* `useQuery()`, respectively. The two mapping functions convert the respective response
21+
* data into a common target type `T`. The secondary response is used as a fallback when
22+
* the primary response data is `undefined`.
23+
*
24+
* @template T The target type that both mappers should produce
25+
* @template D1 The data type of Response<D1>["data"]
26+
* @template D2 The data type of Response<D2>["data"]
27+
* @template R1 The primary response type, typically `UseSubscriptionResponse<D1>`
28+
* @template R2 The fallback response type, typically `UseQueryResponse<D2>`
29+
*
30+
*/
31+
export interface UseMappedWithFallbackOptions<
32+
T,
33+
D1,
34+
D2,
35+
R1 extends Response<D1>,
36+
R2 extends Response<D2>,
37+
> {
38+
/** The primary response source, typically returned by `useSubscription()` */
39+
response1: R1;
40+
41+
/** The fallback response source, typically returned by `useQuery()` */
42+
response2: R2;
43+
44+
/** Mapping function for the primary response data. `d` is `response1.data` */
45+
map1: (d: Ref<D1 | undefined>) => T | undefined;
46+
47+
/** Mapping function for the fallback response data. `d` is `response2.data` */
48+
map2: (d: Ref<D2 | undefined>) => T | undefined;
49+
}
50+
51+
/**
52+
* Return type of `useMappedWithFallback()`.
53+
*
54+
* The returned object contains two reactive properties: `data` and `error`.
55+
*/
56+
interface UseMappedWithFallbackReturn<T> {
57+
/**
58+
* The reactive value mapped primarily from `response1` with fallback to `response2`.
59+
* `undefined` if either response has an error.
60+
*/
61+
data: ComputedRef<T | undefined>;
62+
63+
/**
64+
* The error from `response1` otherwise from `response2` else `undefined`
65+
*/
66+
error: ComputedRef<Error | undefined>;
67+
}
68+
69+
/**
70+
* A reactive mapped value from a primary response with secondary fallback.
71+
*
72+
* @param options {@link UseMappedWithFallbackOptions}
73+
* @returns {@link UseMappedWithFallbackReturn}
74+
* @example
75+
* ```typescript
76+
* const subscription = useSubscription<D1>({ query: SUBSCRIPTION_QUERY });
77+
* const query = useQuery<D2>({ query: QUERY });
78+
*
79+
* const { data, error } = useMappedWithFallback({
80+
* response1: subscription, // primary: real-time updates
81+
* response2: query, // fallback: initial data
82+
* map1: (d) => d.value?.ctrlState,
83+
* map2: (d) => d.value?.ctrl.state,
84+
* });
85+
* ```
86+
*/
87+
export function useMappedWithFallback<
88+
T,
89+
D1,
90+
D2,
91+
R1 extends Response<D1>,
92+
R2 extends Response<D2>,
93+
>(
94+
options: UseMappedWithFallbackOptions<T, D1, D2, R1, R2>,
95+
): UseMappedWithFallbackReturn<T> {
96+
const error = computed(
97+
() => options.response1.error?.value ?? options.response2.error?.value,
98+
);
99+
100+
const data = computed(() =>
101+
error.value
102+
? undefined
103+
: (options.map1(options.response1.data) ?? options.map2(options.response2.data)),
104+
);
105+
106+
return { data, error };
107+
}

src/api/use-run-no-subscription.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { ComputedRef } from "vue";
2-
import { computed } from "vue";
32

43
import {
54
useCtrlRunNoQuery,
@@ -8,31 +7,36 @@ import {
87
import { onReady } from "@/utils/on-ready";
98
import type { OnReady } from "@/utils/on-ready";
109

10+
import { useMappedWithFallback } from "./use-mapped-with-fallback";
11+
12+
type Query = ReturnType<typeof useCtrlRunNoQuery>;
13+
type Subscription = ReturnType<typeof useCtrlRunNoSSubscription>;
14+
1115
interface _RunNoSubscription {
1216
runNo: ComputedRef<number | undefined>;
1317
error: ComputedRef<Error | undefined>;
14-
subscription: ReturnType<typeof useCtrlRunNoSSubscription>;
15-
query: ReturnType<typeof useCtrlRunNoQuery>;
18+
subscription: Subscription;
19+
query: Query;
1620
}
1721

1822
type RunNoSubscription = OnReady<_RunNoSubscription>;
1923

2024
export function useSubscribeRunNo(): RunNoSubscription {
21-
const query = useCtrlRunNoQuery({
22-
requestPolicy: "network-only",
23-
variables: {},
24-
});
25+
const query = useCtrlRunNoQuery({ requestPolicy: "network-only", variables: {} });
2526
const subscription = useCtrlRunNoSSubscription({ variables: {} });
2627

27-
const error = computed(() => subscription.error?.value || query.error?.value);
28+
const mapQueryData = (d: typeof query.data) => d.value?.ctrl.runNo;
29+
const mapSubscriptionData = (d: typeof subscription.data) => d.value?.ctrlRunNo;
2830

29-
const runNo = computed(() =>
30-
error.value
31-
? undefined
32-
: subscription.data?.value?.ctrlRunNo || query.data?.value?.ctrl.runNo,
33-
);
31+
const options = {
32+
response1: subscription,
33+
response2: query,
34+
map1: mapSubscriptionData,
35+
map2: mapQueryData,
36+
};
37+
const { data, error } = useMappedWithFallback(options);
3438

35-
const ret = { runNo, error, subscription, query };
39+
const ret = { runNo: data, error, subscription, query };
3640

3741
return onReady(ret, query);
3842
}

src/api/use-state-subscription.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { ComputedRef } from "vue";
2-
import { computed } from "vue";
32

43
import {
54
useCtrlStateQuery,
@@ -8,31 +7,36 @@ import {
87
import { onReady } from "@/utils/on-ready";
98
import type { OnReady } from "@/utils/on-ready";
109

10+
import { useMappedWithFallback } from "./use-mapped-with-fallback";
11+
12+
type Query = ReturnType<typeof useCtrlStateQuery>;
13+
type Subscription = ReturnType<typeof useCtrlStateSSubscription>;
14+
1115
interface _StateSubscription {
1216
state: ComputedRef<string | undefined>;
1317
error: ComputedRef<Error | undefined>;
14-
subscription: ReturnType<typeof useCtrlStateSSubscription>;
15-
query: ReturnType<typeof useCtrlStateQuery>;
18+
subscription: Subscription;
19+
query: Query;
1620
}
1721

1822
type StateSubscription = OnReady<_StateSubscription>;
1923

2024
export function useSubscribeState(): StateSubscription {
21-
const query = useCtrlStateQuery({
22-
requestPolicy: "network-only",
23-
variables: {},
24-
});
25+
const query = useCtrlStateQuery({ requestPolicy: "network-only", variables: {} });
2526
const subscription = useCtrlStateSSubscription({ variables: {} });
2627

27-
const error = computed(() => subscription.error?.value || query.error?.value);
28+
const mapQueryData = (d: typeof query.data) => d.value?.ctrl.state;
29+
const mapSubscriptionData = (d: typeof subscription.data) => d.value?.ctrlState;
2830

29-
const state = computed(() =>
30-
error.value
31-
? undefined
32-
: subscription.data?.value?.ctrlState || query.data?.value?.ctrl.state,
33-
);
31+
const options = {
32+
response1: subscription,
33+
response2: query,
34+
map1: mapSubscriptionData,
35+
map2: mapQueryData,
36+
};
37+
const { data, error } = useMappedWithFallback(options);
3438

35-
const ret = { state, error, subscription, query };
39+
const ret = { state: data, error, subscription, query };
3640

3741
return onReady(ret, query);
3842
}

0 commit comments

Comments
 (0)