Skip to content

Commit dbf613b

Browse files
authored
feat: DH-16737 Add ObjectManager, useWidget hook (#2030) (#2056)
- Hook for loading a widget that makes it easy to use - `ObjectManager` context allows for implementation to handle loading the object - On Core side, we have a very simple `ObjectManager`, as we just have one connection - On Enterprise side, the `ObjectManager` provided to the context manager will need to handle fetching from queries, and will be able to handle more scenarios (such as when a query is restarting) - Use with deephaven/deephaven-plugins#502
1 parent 496a0d0 commit dbf613b

9 files changed

Lines changed: 476 additions & 5 deletions

File tree

package-lock.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/app-utils/src/components/ConnectionBootstrap.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import React, { useCallback, useEffect, useState } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
22
import { LoadingOverlay } from '@deephaven/components';
33
import {
44
ObjectFetcherContext,
5+
ObjectFetchManager,
6+
ObjectFetchManagerContext,
57
sanitizeVariableDescriptor,
68
useApi,
79
useClient,
@@ -31,6 +33,7 @@ export function ConnectionBootstrap({
3133
const client = useClient();
3234
const [error, setError] = useState<unknown>();
3335
const [connection, setConnection] = useState<dh.IdeConnection>();
36+
3437
useEffect(
3538
function initConnection() {
3639
let isCanceled = false;
@@ -83,6 +86,24 @@ export function ConnectionBootstrap({
8386
[connection]
8487
);
8588

89+
/** We don't really need to do anything fancy in Core to manage an object, just fetch it */
90+
const objectManager: ObjectFetchManager = useMemo(
91+
() => ({
92+
subscribe: (descriptor, onUpdate) => {
93+
// We send an update with the fetch right away
94+
onUpdate({
95+
fetch: () => objectFetcher(descriptor),
96+
status: 'ready',
97+
});
98+
return () => {
99+
// no-op
100+
// For Core, if the server dies then we can't reconnect anyway, so no need to bother listening for subscription or cleaning up
101+
};
102+
},
103+
}),
104+
[objectFetcher]
105+
);
106+
86107
if (connection == null || error != null) {
87108
return (
88109
<LoadingOverlay
@@ -96,7 +117,9 @@ export function ConnectionBootstrap({
96117
return (
97118
<ConnectionContext.Provider value={connection}>
98119
<ObjectFetcherContext.Provider value={objectFetcher}>
99-
{children}
120+
<ObjectFetchManagerContext.Provider value={objectManager}>
121+
{children}
122+
</ObjectFetchManagerContext.Provider>
100123
</ObjectFetcherContext.Provider>
101124
</ConnectionContext.Provider>
102125
);

packages/jsapi-bootstrap/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"@deephaven/components": "file:../components",
2626
"@deephaven/jsapi-types": "1.0.0-dev0.34.0",
2727
"@deephaven/log": "file:../log",
28-
"@deephaven/react-hooks": "file:../react-hooks"
28+
"@deephaven/react-hooks": "file:../react-hooks",
29+
"@deephaven/utils": "file:../utils"
2930
},
3031
"devDependencies": {
3132
"react": "^17.x"

packages/jsapi-bootstrap/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ export * from './DeferredApiBootstrap';
44
export * from './useApi';
55
export * from './useClient';
66
export * from './useDeferredApi';
7+
export * from './useObjectFetch';
78
export * from './useObjectFetcher';
9+
export * from './useWidget';
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from 'react';
2+
import { renderHook } from '@testing-library/react-hooks';
3+
import { ObjectFetchManagerContext, useObjectFetch } from './useObjectFetch';
4+
5+
it('should resolve the objectFetch when in the context', async () => {
6+
const objectFetch = jest.fn(async () => undefined);
7+
const unsubscribe = jest.fn();
8+
const descriptor = { type: 'type', name: 'name' };
9+
const subscribe = jest.fn((subscribeDescriptor, onUpdate) => {
10+
expect(subscribeDescriptor).toEqual(descriptor);
11+
onUpdate({ fetch: objectFetch, status: 'ready' });
12+
return unsubscribe;
13+
});
14+
const objectManager = { subscribe };
15+
const wrapper = ({ children }) => (
16+
<ObjectFetchManagerContext.Provider value={objectManager}>
17+
{children}
18+
</ObjectFetchManagerContext.Provider>
19+
);
20+
21+
const { result } = renderHook(() => useObjectFetch(descriptor), { wrapper });
22+
expect(result.current).toEqual({ fetch: objectFetch, status: 'ready' });
23+
expect(result.error).toBeUndefined();
24+
expect(objectFetch).not.toHaveBeenCalled();
25+
});
26+
27+
it('should return an error, not throw if objectFetch not available in the context', async () => {
28+
const descriptor = { type: 'type', name: 'name' };
29+
const { result } = renderHook(() => useObjectFetch(descriptor));
30+
expect(result.current).toEqual({
31+
error: expect.any(Error),
32+
status: 'error',
33+
});
34+
expect(result.error).toBeUndefined();
35+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { createContext, useContext, useEffect, useState } from 'react';
2+
import type { dh } from '@deephaven/jsapi-types';
3+
4+
/** Function for unsubscribing from a given subscription */
5+
export type UnsubscribeFunction = () => void;
6+
7+
/** Update when the ObjectFetch is still loading */
8+
export type ObjectFetchLoading = {
9+
status: 'loading';
10+
};
11+
12+
/** Update when the ObjectFetch has errored */
13+
export type ObjectFetchError = {
14+
error: NonNullable<unknown>;
15+
status: 'error';
16+
};
17+
18+
/** Update when the object is ready */
19+
export type ObjectFetchReady<T> = {
20+
fetch: () => Promise<T>;
21+
status: 'ready';
22+
};
23+
24+
/**
25+
* Update with the current `fetch` function and status of the object.
26+
* - If both `fetch` and `error` are `null`, it is still loading the fetcher
27+
* - If `fetch` is not `null`, the object is ready to be fetched
28+
* - If `error` is not `null`, there was an error loading the object
29+
*/
30+
export type ObjectFetchUpdate<T = unknown> =
31+
| ObjectFetchLoading
32+
| ObjectFetchError
33+
| ObjectFetchReady<T>;
34+
35+
export type ObjectFetchUpdateCallback<T = unknown> = (
36+
update: ObjectFetchUpdate<T>
37+
) => void;
38+
39+
/** ObjectFetchManager for managing a subscription to an object using a VariableDescriptor */
40+
export type ObjectFetchManager = {
41+
/**
42+
* Subscribe to the fetch function for an object using a variable descriptor.
43+
* It's possible that the fetch function changes over time, due to disconnection/reconnection, starting/stopping of applications that the object may be associated with, etc.
44+
*
45+
* @param descriptor Descriptor object of the object to fetch. Can be extended by a specific implementation to include more details necessary for the ObjectManager.
46+
* @param onUpdate Callback function to be called when the object is updated.
47+
* @returns An unsubscribe function to stop listening for fetch updates and clean up the object.
48+
*/
49+
subscribe: <T = unknown>(
50+
descriptor: dh.ide.VariableDescriptor,
51+
onUpdate: ObjectFetchUpdateCallback<T>
52+
) => UnsubscribeFunction;
53+
};
54+
55+
/** Context for tracking an implementation of the ObjectFetchManager. */
56+
export const ObjectFetchManagerContext =
57+
createContext<ObjectFetchManager | null>(null);
58+
59+
/**
60+
* Retrieve a `fetch` function for the given variable descriptor.
61+
*
62+
* @param descriptor Descriptor to get the `fetch` function for
63+
* @returns An object with the current `fetch` function, OR an error status set if there was an issue fetching the object.
64+
* Retrying is left up to the ObjectManager implementation used from this context.
65+
*/
66+
export function useObjectFetch<T = unknown>(
67+
descriptor: dh.ide.VariableDescriptor
68+
): ObjectFetchUpdate<T> {
69+
const [currentUpdate, setCurrentUpdate] = useState<ObjectFetchUpdate<T>>({
70+
status: 'loading',
71+
});
72+
73+
const objectFetchManager = useContext(ObjectFetchManagerContext);
74+
75+
useEffect(() => {
76+
if (objectFetchManager == null) {
77+
setCurrentUpdate({
78+
error: new Error('No ObjectFetchManager available in context'),
79+
status: 'error',
80+
});
81+
return;
82+
}
83+
// Update to signal we're still loading, if we're not already in a loading state.
84+
setCurrentUpdate(oldUpdate =>
85+
oldUpdate.status === 'loading' ? oldUpdate : { status: 'loading' }
86+
);
87+
return objectFetchManager.subscribe(descriptor, setCurrentUpdate);
88+
}, [descriptor, objectFetchManager]);
89+
90+
return currentUpdate;
91+
}

0 commit comments

Comments
 (0)