Skip to content

Commit 29696ba

Browse files
committed
add app query hooks
1 parent ea611c1 commit 29696ba

File tree

12 files changed

+438
-2
lines changed

12 files changed

+438
-2
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script lang="ts">
2+
import type { Snippet } from 'svelte';
3+
import { QueryClientProvider, QueryClient } from '@tanstack/svelte-query';
4+
import { provideViamClient } from '../hooks/app/use-app-client.svelte';
5+
import type { Credentials } from '@viamrobotics/sdk';
6+
7+
interface Props {
8+
client?: QueryClient;
9+
serviceHost: string;
10+
credentials: Credentials;
11+
children?: Snippet;
12+
}
13+
14+
let {
15+
client = new QueryClient(),
16+
serviceHost,
17+
credentials,
18+
children,
19+
}: Props = $props();
20+
21+
provideViamClient(() => ({ serviceHost, credentials }));
22+
</script>
23+
24+
<QueryClientProvider {client}>
25+
{@render children?.()}
26+
</QueryClientProvider>
File renamed without changes.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { createMutation, type MutationOptions } from '@tanstack/svelte-query';
2+
import type { AppClient } from '@viamrobotics/sdk';
3+
4+
import type { ArgumentsType, ResolvedReturnType } from './types';
5+
import { fromStore, toStore } from 'svelte/store';
6+
import { useQueryLogger } from '$lib/query-logger';
7+
import { useViamClient } from './use-app-client.svelte';
8+
9+
export const createAppMutation = <T extends AppClient, K extends keyof T>(
10+
method: K
11+
) => {
12+
type MutArgs = ArgumentsType<T[K]>;
13+
type MutReturn = ResolvedReturnType<T[K]>;
14+
15+
const viamClient = useViamClient();
16+
const appClient = $derived(viamClient.current?.appClient as T);
17+
const debug = useQueryLogger();
18+
19+
const methodName = $derived(String(method));
20+
21+
const mutationOptions = $derived({
22+
mutationKey: ['viam-svelte-app-sdk', 'appClient', methodName],
23+
mutationFn: async (request) => {
24+
const clientFunc = appClient?.[method];
25+
26+
if (typeof clientFunc !== 'function') {
27+
throw new TypeError(
28+
`${String(method)} is not a method on the resource client.`
29+
);
30+
}
31+
32+
const logger = debug.createLogger();
33+
logger('REQ', 'appClient', methodName, request);
34+
35+
try {
36+
const response = (await clientFunc.apply(
37+
appClient,
38+
request
39+
)) as Promise<MutReturn>;
40+
41+
logger('RES', 'appClient', methodName, response);
42+
return response;
43+
} catch (error) {
44+
logger('ERR', 'appClient', methodName, error);
45+
throw error;
46+
}
47+
},
48+
} satisfies MutationOptions<MutReturn, Error, MutArgs>);
49+
50+
return fromStore(createMutation(toStore(() => mutationOptions)));
51+
};
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
createQuery,
3+
queryOptions as createQueryOptions,
4+
type QueryObserverResult,
5+
} from '@tanstack/svelte-query';
6+
import type { AppClient } from '@viamrobotics/sdk';
7+
import { toStore, fromStore } from 'svelte/store';
8+
import { usePolling } from '../use-polling.svelte';
9+
import { useQueryLogger } from '../../query-logger';
10+
import { useViamClient } from './use-app-client.svelte';
11+
import type { ArgumentsType, ResolvedReturnType } from './types';
12+
13+
interface QueryOptions {
14+
// enabled defaults to true if unspecified
15+
enabled?: boolean;
16+
refetchInterval: number | false;
17+
refetchIntervalInBackground?: boolean;
18+
}
19+
20+
export const createAppQuery = <T extends AppClient, K extends keyof T>(
21+
method: K,
22+
...additional:
23+
| [
24+
args?: (() => ArgumentsType<T[K]>) | ArgumentsType<T[K]>,
25+
options?: (() => QueryOptions) | QueryOptions,
26+
]
27+
| [options?: (() => QueryOptions) | QueryOptions]
28+
): { current: QueryObserverResult<ResolvedReturnType<T[K]>> } => {
29+
const viamClient = useViamClient();
30+
const appClient = $derived(viamClient.current?.appClient as T);
31+
const debug = useQueryLogger();
32+
33+
let [args, options] = additional;
34+
35+
if (options === undefined && args !== undefined) {
36+
options = args as QueryOptions;
37+
args = undefined;
38+
}
39+
40+
const _options = $derived(
41+
typeof options === 'function' ? options() : options
42+
);
43+
const _args = $derived(typeof args === 'function' ? args() : args);
44+
const methodName = $derived(String(method));
45+
46+
const queryOptions = $derived(
47+
createQueryOptions({
48+
queryKey: [
49+
'viam-svelte-app-sdk',
50+
'appClient',
51+
methodName,
52+
...(_args ? [_args] : []),
53+
],
54+
enabled: appClient !== undefined && _options?.enabled !== false,
55+
queryFn: async () => {
56+
if (!appClient) {
57+
throw new Error('appClient is undefined');
58+
}
59+
60+
const clientFunc = appClient[method];
61+
62+
if (typeof clientFunc !== 'function') {
63+
throw new TypeError(
64+
`${String(method)} is not a method on the resource client.`
65+
);
66+
}
67+
68+
const logger = debug.createLogger();
69+
logger('REQ', 'appClient', methodName, _args);
70+
71+
try {
72+
const response = (await clientFunc.apply(
73+
appClient,
74+
_args
75+
)) as Promise<ResolvedReturnType<T[K]>>;
76+
77+
logger('RES', 'appClient', methodName, response);
78+
return response;
79+
} catch (error) {
80+
logger('ERR', 'appClient', methodName, error);
81+
throw error;
82+
}
83+
},
84+
..._options,
85+
refetchInterval: false,
86+
})
87+
);
88+
89+
usePolling(
90+
() => queryOptions.queryKey,
91+
() => _options?.refetchInterval ?? false
92+
);
93+
94+
return fromStore(createQuery(toStore(() => queryOptions)));
95+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { createMutation, type MutationOptions } from '@tanstack/svelte-query';
2+
import type { DataClient } from '@viamrobotics/sdk';
3+
4+
import type { ArgumentsType, ResolvedReturnType } from './types';
5+
import { fromStore, toStore } from 'svelte/store';
6+
import { useQueryLogger } from '$lib/query-logger';
7+
import { useViamClient } from './use-app-client.svelte';
8+
9+
export const createDataMutation = <T extends DataClient, K extends keyof T>(
10+
method: K
11+
) => {
12+
type MutArgs = ArgumentsType<T[K]>;
13+
type MutReturn = ResolvedReturnType<T[K]>;
14+
15+
const viamClient = useViamClient();
16+
const dataClient = $derived(viamClient.current?.dataClient as T);
17+
const debug = useQueryLogger();
18+
19+
const methodName = $derived(String(method));
20+
21+
const mutationOptions = $derived({
22+
mutationKey: ['viam-svelte-app-sdk', 'dataClient', methodName],
23+
mutationFn: async (request) => {
24+
const clientFunc = dataClient?.[method];
25+
26+
if (typeof clientFunc !== 'function') {
27+
throw new TypeError(
28+
`${String(method)} is not a method on the resource client.`
29+
);
30+
}
31+
32+
const logger = debug.createLogger();
33+
logger('REQ', 'dataClient', methodName, request);
34+
35+
try {
36+
const response = (await clientFunc.apply(
37+
dataClient,
38+
request
39+
)) as Promise<MutReturn>;
40+
41+
logger('RES', 'dataClient', methodName, response);
42+
return response;
43+
} catch (error) {
44+
logger('ERR', 'dataClient', methodName, error);
45+
throw error;
46+
}
47+
},
48+
} satisfies MutationOptions<MutReturn, Error, MutArgs>);
49+
50+
return fromStore(createMutation(toStore(() => mutationOptions)));
51+
};
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
createQuery,
3+
queryOptions as createQueryOptions,
4+
type QueryObserverResult,
5+
} from '@tanstack/svelte-query';
6+
import type { DataClient } from '@viamrobotics/sdk';
7+
import { toStore, fromStore } from 'svelte/store';
8+
import { usePolling } from '../use-polling.svelte';
9+
import { useQueryLogger } from '../../query-logger';
10+
import { useViamClient } from './use-app-client.svelte';
11+
import type { ArgumentsType, ResolvedReturnType } from './types';
12+
13+
interface QueryOptions {
14+
// enabled defaults to true if unspecified
15+
enabled?: boolean;
16+
refetchInterval: number | false;
17+
refetchIntervalInBackground?: boolean;
18+
}
19+
20+
export const createDataQuery = <T extends DataClient, K extends keyof T>(
21+
method: K,
22+
...additional:
23+
| [
24+
args?: (() => ArgumentsType<T[K]>) | ArgumentsType<T[K]>,
25+
options?: (() => QueryOptions) | QueryOptions,
26+
]
27+
| [options?: (() => QueryOptions) | QueryOptions]
28+
): { current: QueryObserverResult<ResolvedReturnType<T[K]>> } => {
29+
const viamClient = useViamClient();
30+
const dataClient = $derived(viamClient.current?.dataClient as T);
31+
const debug = useQueryLogger();
32+
33+
let [args, options] = additional;
34+
35+
if (options === undefined && args !== undefined) {
36+
options = args as QueryOptions;
37+
args = undefined;
38+
}
39+
40+
const _options = $derived(
41+
typeof options === 'function' ? options() : options
42+
);
43+
const _args = $derived(typeof args === 'function' ? args() : args);
44+
const methodName = $derived(String(method));
45+
46+
const queryOptions = $derived(
47+
createQueryOptions({
48+
queryKey: [
49+
'viam-svelte-app-sdk',
50+
'dataClient',
51+
methodName,
52+
...(_args ? [_args] : []),
53+
],
54+
enabled: dataClient !== undefined && _options?.enabled !== false,
55+
queryFn: async () => {
56+
if (!dataClient) {
57+
throw new Error('dataClient is undefined');
58+
}
59+
60+
const clientFunc = dataClient[method];
61+
62+
if (typeof clientFunc !== 'function') {
63+
throw new TypeError(
64+
`${String(method)} is not a method on the resource client.`
65+
);
66+
}
67+
68+
const logger = debug.createLogger();
69+
logger('REQ', 'dataClient', methodName, _args);
70+
71+
try {
72+
const response = (await clientFunc.apply(
73+
dataClient,
74+
_args
75+
)) as Promise<ResolvedReturnType<T[K]>>;
76+
77+
logger('RES', 'dataClient', methodName, response);
78+
return response;
79+
} catch (error) {
80+
logger('ERR', 'dataClient', methodName, error);
81+
throw error;
82+
}
83+
},
84+
..._options,
85+
refetchInterval: false,
86+
})
87+
);
88+
89+
usePolling(
90+
() => queryOptions.queryKey,
91+
() => _options?.refetchInterval ?? false
92+
);
93+
94+
return fromStore(createQuery(toStore(() => queryOptions)));
95+
};

src/lib/hooks/app/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2+
export type ArgumentsType<T> = T extends (...args: infer U) => any ? U : never;
3+
4+
export type ResolvedReturnType<T> = T extends (
5+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6+
...args: any[]
7+
) => Promise<infer R>
8+
? R
9+
: never;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {
2+
createViamClient,
3+
type Credentials,
4+
type ViamClient,
5+
} from '@viamrobotics/sdk';
6+
import { getContext, setContext } from 'svelte';
7+
8+
const key = Symbol('viam-app-context');
9+
10+
interface Context {
11+
current: ViamClient | undefined;
12+
connectionError: Error | undefined;
13+
}
14+
15+
export interface ViamAppClientOptions {
16+
serviceHost: string;
17+
credentials: Credentials;
18+
}
19+
20+
export const provideViamClient = (
21+
viamClientOptions: () => ViamAppClientOptions | undefined
22+
) => {
23+
let client = $state.raw<ViamClient>();
24+
let connectionError = $state.raw<Error>();
25+
26+
const connect = async (args: ViamAppClientOptions) => {
27+
try {
28+
client = await createViamClient(args);
29+
} catch (error) {
30+
console.error(error);
31+
connectionError = error as Error;
32+
}
33+
};
34+
35+
$effect(() => {
36+
const options = viamClientOptions();
37+
38+
if (options) {
39+
connect(options);
40+
}
41+
});
42+
43+
setContext<Context>(key, {
44+
get current() {
45+
return client;
46+
},
47+
get connectionError() {
48+
return connectionError;
49+
},
50+
});
51+
};
52+
53+
export const useViamClient = (): {
54+
current: ViamClient | undefined;
55+
} => {
56+
return getContext<Context>(key);
57+
};

0 commit comments

Comments
 (0)