Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: DH-16737 Add ObjectManager, useWidget hook #2030

Merged
merged 8 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
});
mofojed marked this conversation as resolved.
Show resolved Hide resolved
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
Loading