Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion packages/app-utils/src/components/ConnectionBootstrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type ObjectFetchManager,
ObjectFetchManagerContext,
sanitizeVariableDescriptor,
type UriVariableDescriptor,
useApi,
useClient,
} from '@deephaven/jsapi-bootstrap';
Expand Down Expand Up @@ -168,8 +169,11 @@ export function ConnectionBootstrap({
);

const objectFetcher = useCallback(
async (descriptor: dh.ide.VariableDescriptor) => {
async (descriptor: dh.ide.VariableDescriptor | UriVariableDescriptor) => {
assertNotNull(connection, 'No connection available to fetch object with');
if (typeof descriptor === 'string') {
throw new Error('No URI resolvers available in Core');
}
return connection.getObject(sanitizeVariableDescriptor(descriptor));
},
[connection]
Expand Down
15 changes: 8 additions & 7 deletions packages/jsapi-bootstrap/src/useDeferredApi.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { createContext, useContext, useEffect, useState } from 'react';
import type { dh as DhType } from '@deephaven/jsapi-types';
import { ApiContext } from './ApiBootstrap';
import { type UriVariableDescriptor } from './useObjectFetcher';

/**
* Function to fetch an API based on a provided descriptor object.
* Depending on the context there may be more properties on the descriptor,
* providing more information about the object, such as a session ID.
* @param descriptor Descriptor object to fetch the API from.
* @param descriptor Descriptor object or URI to fetch the API from.
* @returns A promise that resolves to the API instance for the provided variable descriptor.
*/
export type DeferredApiFetcher = (
descriptor: DhType.ide.VariableDescriptor
descriptor: DhType.ide.VariableDescriptor | UriVariableDescriptor
) => Promise<typeof DhType>;

export const DeferredApiContext = createContext<
Expand All @@ -20,14 +21,14 @@ export const DeferredApiContext = createContext<
/**
* Retrieve the API for the current context, given the widget provided.
* The API may need to be loaded, and will return `null` until it is ready.
* @param widget The widget descriptor to use to fetch the API
* @param widget The widget descriptor or URI to use to fetch the API
* @returns A tuple with the API instance, and an error if one occurred.
*/
export function useDeferredApi(
widget: DhType.ide.VariableDescriptor | null
): [dh: typeof DhType | null, error: unknown | null] {
widget: DhType.ide.VariableDescriptor | UriVariableDescriptor | null
): [dh: typeof DhType | null, error: NonNullable<unknown> | null] {
const [api, setApi] = useState<typeof DhType | null>(null);
const [error, setError] = useState<unknown | null>(null);
const [error, setError] = useState<NonNullable<unknown> | null>(null);
const deferredApi = useContext(DeferredApiContext);
const contextApi = useContext(ApiContext);

Expand Down Expand Up @@ -64,7 +65,7 @@ export function useDeferredApi(
} catch (e) {
if (!isCancelled) {
setApi(null);
setError(e);
setError(e ?? new Error('Null error'));
}
}
} else {
Expand Down
9 changes: 5 additions & 4 deletions packages/jsapi-bootstrap/src/useObjectFetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createContext, useContext, useEffect, useState } from 'react';
import type { dh } from '@deephaven/jsapi-types';
import { type UriVariableDescriptor } from './useObjectFetcher';

/** Function for unsubscribing from a given subscription */
export type UnsubscribeFunction = () => void;
Expand Down Expand Up @@ -42,12 +43,12 @@ 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 descriptor Descriptor object or URI 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,
descriptor: dh.ide.VariableDescriptor | UriVariableDescriptor,
onUpdate: ObjectFetchUpdateCallback<T>
) => UnsubscribeFunction;
};
Expand All @@ -59,12 +60,12 @@ export const ObjectFetchManagerContext =
/**
* Retrieve a `fetch` function for the given variable descriptor.
*
* @param descriptor Descriptor to get the `fetch` function for
* @param descriptor Descriptor or URI 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
descriptor: dh.ide.VariableDescriptor | UriVariableDescriptor
): ObjectFetchUpdate<T> {
const [currentUpdate, setCurrentUpdate] = useState<ObjectFetchUpdate<T>>({
status: 'loading',
Expand Down
8 changes: 5 additions & 3 deletions packages/jsapi-bootstrap/src/useObjectFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export type IdVariableDescriptor = {
id: string;
};

export type UriVariableDescriptor = string;

export function isNameVariableDescriptor(
value: unknown
): value is NameVariableDescriptor {
Expand Down Expand Up @@ -52,11 +54,11 @@ export function isVariableDescriptor(

/**
* Function to fetch an object based on a provided descriptor object.
* @param descriptor Descriptor object to fetch the object from. Can be extended by a specific implementation to
* include additional fields (such as a session ID) to uniquely identify an object.
* @param descriptor Descriptor object or URI to fetch the object from. Can be extended by a specific implementation to
* include additional fields (such as a session ID) to uniquely identify an object.
*/
export type ObjectFetcher = <T = unknown>(
descriptor: dh.ide.VariableDescriptor
descriptor: dh.ide.VariableDescriptor | UriVariableDescriptor
) => Promise<T>;

export const ObjectFetcherContext = createContext<ObjectFetcher | null>(null);
Expand Down
121 changes: 80 additions & 41 deletions packages/jsapi-bootstrap/src/useWidget.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,37 @@ import { act, renderHook } from '@testing-library/react-hooks';
import { type dh } from '@deephaven/jsapi-types';
import { TestUtils } from '@deephaven/test-utils';
import { useWidget } from './useWidget';
import { ObjectFetchManagerContext } from './useObjectFetch';
import {
type ObjectFetchManager,
ObjectFetchManagerContext,
} from './useObjectFetch';
import { ApiContext } from './ApiBootstrap';
import { DeferredApiContext } from './useDeferredApi';

const WIDGET_TYPE = 'OtherWidget';

function makeWrapper(
objectManager: ObjectFetchManager,
api = jest.fn(),
deferredApi?: jest.Mock
) {
return function Wrapper({ children }: React.PropsWithChildren<never>) {
return (
<ObjectFetchManagerContext.Provider value={objectManager}>
<ApiContext.Provider value={api as unknown as typeof dh}>
{deferredApi ? (
<DeferredApiContext.Provider value={deferredApi}>
{children}
</DeferredApiContext.Provider>
) : (
children
)}
</ApiContext.Provider>
</ObjectFetchManagerContext.Provider>
);
};
}

describe('useWidget', () => {
it('should return a widget when available', async () => {
const descriptor: dh.ide.VariableDescriptor = {
Expand All @@ -22,14 +49,45 @@ describe('useWidget', () => {
return jest.fn();
});
const objectManager = { subscribe };
const wrapper = ({ children }) => (
<ObjectFetchManagerContext.Provider value={objectManager}>
{children}
</ObjectFetchManagerContext.Provider>
const api = jest.fn();
const wrapper = makeWrapper(objectManager, api);
const { result } = renderHook(() => useWidget(descriptor), { wrapper });
await act(TestUtils.flushPromises);
expect(result.current).toEqual({
widget,
error: null,
api,
});
expect(fetch).toHaveBeenCalledTimes(1);
});

it('should use the deferred API if available', async () => {
const descriptor: dh.ide.VariableDescriptor = {
type: 'OtherWidget',
name: 'name',
};
const widget = { close: jest.fn() };
const fetch = jest.fn(async () => widget);
const objectFetch = { fetch, error: null };
const subscribe = jest.fn((subscribeDescriptor, onUpdate) => {
expect(subscribeDescriptor).toEqual(descriptor);
onUpdate(objectFetch);
return jest.fn();
});
const objectManager = { subscribe };
const deferredApi = {};
const wrapper = makeWrapper(
objectManager,
jest.fn(),
jest.fn(() => deferredApi)
);
const { result } = renderHook(() => useWidget(descriptor), { wrapper });
await act(TestUtils.flushPromises);
expect(result.current).toEqual({ widget, error: null });
expect(result.current).toEqual({
widget,
error: null,
api: deferredApi,
});
expect(fetch).toHaveBeenCalledTimes(1);
});

Expand All @@ -46,15 +104,11 @@ describe('useWidget', () => {
return jest.fn();
});
const objectManager = { subscribe };
const wrapper = ({ children }) => (
<ObjectFetchManagerContext.Provider value={objectManager}>
{children}
</ObjectFetchManagerContext.Provider>
);
const wrapper = makeWrapper(objectManager);

const { result } = renderHook(() => useWidget(descriptor), { wrapper });

expect(result.current).toEqual({ widget: null, error });
expect(result.current).toEqual({ widget: null, error, api: null });
});

it('should return null when still loading', () => {
Expand All @@ -65,14 +119,10 @@ describe('useWidget', () => {
return jest.fn();
});
const objectManager = { subscribe };
const wrapper = ({ children }) => (
<ObjectFetchManagerContext.Provider value={objectManager}>
{children}
</ObjectFetchManagerContext.Provider>
);
const wrapper = makeWrapper(objectManager);
const { result } = renderHook(() => useWidget(descriptor), { wrapper });

expect(result.current).toEqual({ widget: null, error: null });
expect(result.current).toEqual({ widget: null, error: null, api: null });
});

it('should close the widget and exported objects when cancelled', async () => {
Expand All @@ -95,19 +145,15 @@ describe('useWidget', () => {
return jest.fn();
});
const objectManager = { subscribe };
const wrapper = ({ children }) => (
<ObjectFetchManagerContext.Provider value={objectManager}>
{children}
</ObjectFetchManagerContext.Provider>
);
const wrapper = makeWrapper(objectManager);
const { result, unmount } = renderHook(() => useWidget(descriptor), {
wrapper,
});
expect(widget.close).not.toHaveBeenCalled();
expect(widget.exportedObjects[0].close).not.toHaveBeenCalled();
expect(widget.exportedObjects[1].close).not.toHaveBeenCalled();

expect(result.current).toEqual({ widget: null, error: null });
expect(result.current).toEqual({ widget: null, error: null, api: null });

// Unmount before flushing the promise
unmount();
Expand Down Expand Up @@ -137,16 +183,12 @@ describe('useWidget', () => {
return jest.fn();
});
const objectManager = { subscribe };
const wrapper = ({ children }) => (
<ObjectFetchManagerContext.Provider value={objectManager}>
{children}
</ObjectFetchManagerContext.Provider>
);
const wrapper = makeWrapper(objectManager);
const { result, unmount } = renderHook(() => useWidget(descriptor), {
wrapper,
});

expect(result.current).toEqual({ widget: null, error: null });
expect(result.current).toEqual({ widget: null, error: null, api: null });
await act(TestUtils.flushPromises);
unmount();
expect(widget.close).not.toHaveBeenCalled();
Expand All @@ -168,16 +210,17 @@ describe('useWidget', () => {
return jest.fn();
});
const objectManager = { subscribe };
const wrapper = ({ children }) => (
<ObjectFetchManagerContext.Provider value={objectManager}>
{children}
</ObjectFetchManagerContext.Provider>
);
const api = jest.fn();
const wrapper = makeWrapper(objectManager, api);
const { result, unmount } = renderHook(() => useWidget(descriptor), {
wrapper,
});
await act(TestUtils.flushPromises);
expect(result.current).toEqual({ widget: table, error: null });
expect(result.current).toEqual({
widget: table,
error: null,
api,
});
expect(fetch).toHaveBeenCalledTimes(1);
unmount();

Expand All @@ -199,11 +242,7 @@ describe('useWidget', () => {
return jest.fn();
});
const objectManager = { subscribe };
const wrapper = ({ children }) => (
<ObjectFetchManagerContext.Provider value={objectManager}>
{children}
</ObjectFetchManagerContext.Provider>
);
const wrapper = makeWrapper(objectManager);
const { unmount } = renderHook(() => useWidget(descriptor), { wrapper });

unmount();
Expand Down
Loading
Loading