Skip to content

Commit

Permalink
feat: DH-16737 Add ObjectManager, useWidget hook (#2030)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
mofojed committed Jun 4, 2024
1 parent 496a0d0 commit 45c78a0
Show file tree
Hide file tree
Showing 9 changed files with 476 additions and 5 deletions.
4 changes: 3 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 25 additions & 2 deletions packages/app-utils/src/components/ConnectionBootstrap.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { LoadingOverlay } from '@deephaven/components';
import {
ObjectFetcherContext,
ObjectFetchManager,
ObjectFetchManagerContext,
sanitizeVariableDescriptor,
useApi,
useClient,
Expand Down Expand Up @@ -31,6 +33,7 @@ export function ConnectionBootstrap({
const client = useClient();
const [error, setError] = useState<unknown>();
const [connection, setConnection] = useState<dh.IdeConnection>();

useEffect(
function initConnection() {
let isCanceled = false;
Expand Down Expand Up @@ -83,6 +86,24 @@ export function ConnectionBootstrap({
[connection]
);

/** We don't really need to do anything fancy in Core to manage an object, just fetch it */
const objectManager: ObjectFetchManager = useMemo(
() => ({
subscribe: (descriptor, onUpdate) => {
// We send an update with the fetch right away
onUpdate({
fetch: () => objectFetcher(descriptor),
status: 'ready',
});
return () => {
// no-op
// For Core, if the server dies then we can't reconnect anyway, so no need to bother listening for subscription or cleaning up
};
},
}),
[objectFetcher]
);

if (connection == null || error != null) {
return (
<LoadingOverlay
Expand All @@ -96,7 +117,9 @@ export function ConnectionBootstrap({
return (
<ConnectionContext.Provider value={connection}>
<ObjectFetcherContext.Provider value={objectFetcher}>
{children}
<ObjectFetchManagerContext.Provider value={objectManager}>
{children}
</ObjectFetchManagerContext.Provider>
</ObjectFetcherContext.Provider>
</ConnectionContext.Provider>
);
Expand Down
3 changes: 2 additions & 1 deletion packages/jsapi-bootstrap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"@deephaven/components": "file:../components",
"@deephaven/jsapi-types": "1.0.0-dev0.34.0",
"@deephaven/log": "file:../log",
"@deephaven/react-hooks": "file:../react-hooks"
"@deephaven/react-hooks": "file:../react-hooks",
"@deephaven/utils": "file:../utils"
},
"devDependencies": {
"react": "^17.x"
Expand Down
2 changes: 2 additions & 0 deletions packages/jsapi-bootstrap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export * from './DeferredApiBootstrap';
export * from './useApi';
export * from './useClient';
export * from './useDeferredApi';
export * from './useObjectFetch';
export * from './useObjectFetcher';
export * from './useWidget';
35 changes: 35 additions & 0 deletions packages/jsapi-bootstrap/src/useObjectFetch.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { ObjectFetchManagerContext, useObjectFetch } from './useObjectFetch';

it('should resolve the objectFetch when in the context', async () => {
const objectFetch = jest.fn(async () => undefined);
const unsubscribe = jest.fn();
const descriptor = { type: 'type', name: 'name' };
const subscribe = jest.fn((subscribeDescriptor, onUpdate) => {
expect(subscribeDescriptor).toEqual(descriptor);
onUpdate({ fetch: objectFetch, status: 'ready' });
return unsubscribe;
});
const objectManager = { subscribe };
const wrapper = ({ children }) => (
<ObjectFetchManagerContext.Provider value={objectManager}>
{children}
</ObjectFetchManagerContext.Provider>
);

const { result } = renderHook(() => useObjectFetch(descriptor), { wrapper });
expect(result.current).toEqual({ fetch: objectFetch, status: 'ready' });
expect(result.error).toBeUndefined();
expect(objectFetch).not.toHaveBeenCalled();
});

it('should return an error, not throw if objectFetch not available in the context', async () => {
const descriptor = { type: 'type', name: 'name' };
const { result } = renderHook(() => useObjectFetch(descriptor));
expect(result.current).toEqual({
error: expect.any(Error),
status: 'error',
});
expect(result.error).toBeUndefined();
});
91 changes: 91 additions & 0 deletions packages/jsapi-bootstrap/src/useObjectFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { createContext, useContext, useEffect, useState } from 'react';
import type { dh } from '@deephaven/jsapi-types';

/** Function for unsubscribing from a given subscription */
export type UnsubscribeFunction = () => void;

/** Update when the ObjectFetch is still loading */
export type ObjectFetchLoading = {
status: 'loading';
};

/** Update when the ObjectFetch has errored */
export type ObjectFetchError = {
error: NonNullable<unknown>;
status: 'error';
};

/** Update when the object is ready */
export type ObjectFetchReady<T> = {
fetch: () => Promise<T>;
status: 'ready';
};

/**
* Update with the current `fetch` function and status of the object.
* - If both `fetch` and `error` are `null`, it is still loading the fetcher
* - If `fetch` is not `null`, the object is ready to be fetched
* - If `error` is not `null`, there was an error loading the object
*/
export type ObjectFetchUpdate<T = unknown> =
| ObjectFetchLoading
| ObjectFetchError
| ObjectFetchReady<T>;

export type ObjectFetchUpdateCallback<T = unknown> = (
update: ObjectFetchUpdate<T>
) => void;

/** ObjectFetchManager for managing a subscription to an object using a VariableDescriptor */
export type ObjectFetchManager = {
/**
* Subscribe to the fetch function for an object using a variable descriptor.
* 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.
*
* @param descriptor Descriptor object of the object to fetch. Can be extended by a specific implementation to include more details necessary for the ObjectManager.
* @param onUpdate Callback function to be called when the object is updated.
* @returns An unsubscribe function to stop listening for fetch updates and clean up the object.
*/
subscribe: <T = unknown>(
descriptor: dh.ide.VariableDescriptor,
onUpdate: ObjectFetchUpdateCallback<T>
) => UnsubscribeFunction;
};

/** Context for tracking an implementation of the ObjectFetchManager. */
export const ObjectFetchManagerContext =
createContext<ObjectFetchManager | null>(null);

/**
* Retrieve a `fetch` function for the given variable descriptor.
*
* @param descriptor Descriptor to get the `fetch` function for
* @returns An object with the current `fetch` function, OR an error status set if there was an issue fetching the object.
* Retrying is left up to the ObjectManager implementation used from this context.
*/
export function useObjectFetch<T = unknown>(
descriptor: dh.ide.VariableDescriptor
): ObjectFetchUpdate<T> {
const [currentUpdate, setCurrentUpdate] = useState<ObjectFetchUpdate<T>>({
status: 'loading',
});

const objectFetchManager = useContext(ObjectFetchManagerContext);

useEffect(() => {
if (objectFetchManager == null) {
setCurrentUpdate({
error: new Error('No ObjectFetchManager available in context'),
status: 'error',
});
return;
}
// Update to signal we're still loading, if we're not already in a loading state.
setCurrentUpdate(oldUpdate =>
oldUpdate.status === 'loading' ? oldUpdate : { status: 'loading' }
);
return objectFetchManager.subscribe(descriptor, setCurrentUpdate);
}, [descriptor, objectFetchManager]);

return currentUpdate;
}
Loading

0 comments on commit 45c78a0

Please sign in to comment.