Skip to content

Commit

Permalink
Split useDataProvider out into a new hook from useData, tested arbitr…
Browse files Browse the repository at this point in the history
…ary method on data provider engine
  • Loading branch information
tjcouch-sil committed Apr 1, 2023
1 parent d6da885 commit b4e4284
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 35 deletions.
12 changes: 10 additions & 2 deletions extensions/hello-someone/hello-someone.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,18 @@ class GreetingsDataProviderEngine {
/**
* @param {string} selector
*/
async get(selector) {
get = async (selector) => {
if (selector === '*') return this.people;
return this.people[selector.toLowerCase()];
}
};

/** Test method to make sure people can use data providers' custom methods */
// eslint-disable-next-line class-methods-use-this
testRandomMethod = async (things) => {
const result = `Greetings data provider got testRandomMethod! ${things}`;
logger.log(result);
return result;
};
}

exports.activate = async () => {
Expand Down
61 changes: 33 additions & 28 deletions src/renderer/hooks/papi-hooks/useData.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { DataProviderSubscriberOptions } from '@shared/models/IDataProvider';
import usePromise from '@renderer/hooks/papi-hooks/usePromise';
import IDataProvider, {
DataProviderSubscriberOptions,
} from '@shared/models/IDataProvider';
import useEventAsync from '@renderer/hooks/papi-hooks/useEventAsync';
import useEvent from '@renderer/hooks/papi-hooks/useEvent';
import { useCallback, useMemo, useState } from 'react';
import dataProviderService from '@shared/services/DataProviderService';
import { useMemo, useState } from 'react';
import { PEventAsync, PEventHandler } from '@shared/models/PEvent';
import useDataProvider from '@renderer/hooks/papi-hooks/useDataProvider';
import { isString } from '@shared/util/Util';

/**
* Subscribes to run a callback on a data provider's data with specified selector
Expand All @@ -23,40 +24,45 @@ import { PEventAsync, PEventHandler } from '@shared/models/PEvent';
* - `isLoading`: whether the data with the selector is awaiting retrieval from the data provider
*/
function useData<TSelector, TGetData, TSetData>(
dataType: string,
dataType:
| string
| [IDataProvider<TSelector, TGetData, TSetData> | undefined, boolean],
selector: TSelector,
defaultValue: TGetData,
subscriberOptions?: DataProviderSubscriberOptions,
): [TGetData, ((newData: TSetData) => Promise<boolean>) | undefined, boolean] {
// The data from the data provider at this selector
const [data, setDataInternal] = useState<TGetData>(defaultValue);

// Check to see if they passed in the results of a useDataProvider hook
const didReceiveDataProvider = !isString(dataType);

// Get the data provider info for this data type
const [dataProviderInfo] = usePromise(
useCallback(
async () =>
dataProviderService.get<TSelector, TGetData, TSetData>(dataType),
[dataType],
),
undefined,
);
// Note: do nothing if we received a data provider, but still run this hook. We must make sure to run the same number of hooks in all code paths)
let [dataProviderTemp, isDisposedTemp] = useDataProvider<
TSelector,
TGetData,
TSetData
>(!didReceiveDataProvider ? dataType : undefined);

// Disable this hook when the data provider is disposed
const [isDisposed, setIsDisposed] = useState<boolean>(false);
useEvent(
dataProviderInfo && !isDisposed ? dataProviderInfo.onDidDispose : undefined,
useCallback(() => setIsDisposed(true), []),
);
// If we received the data provider, just use it
if (didReceiveDataProvider) {
[dataProviderTemp, isDisposedTemp] = dataType;
}

// Make const variables of the data provider so TypeScript knows they won't change
const dataProvider = dataProviderTemp;
const isDisposed = isDisposedTemp;

// Indicates if the data with the selector is awaiting retrieval from the data provider
const [isLoading, setIsLoading] = useState<boolean>(true);

// Wrap subscribe so we can call it as a normal PEvent in useEvent
const wrappedSubscribeEvent: PEventAsync<TGetData> | undefined = useMemo(
() =>
dataProviderInfo && !isDisposed
dataProvider && !isDisposed
? async (eventCallback: PEventHandler<TGetData>) => {
const unsub = await dataProviderInfo.dataProvider.subscribe(
const unsub = await dataProvider.subscribe(
selector,
(subscriptionData) => {
eventCallback(subscriptionData);
Expand All @@ -73,21 +79,20 @@ function useData<TSelector, TGetData, TSetData>(
};
}
: undefined,
[dataProviderInfo, selector, subscriberOptions, isDisposed],
[dataProvider, selector, subscriberOptions, isDisposed],
);

// Subscribe to the data provider
useEventAsync(wrappedSubscribeEvent, setDataInternal);

// TODO: cache latest setStateAction and fire until we have dataProviderInfo instead of having setData be undefined until we have dataProviderInfo?
// TODO: cache latest setStateAction and fire until we have dataProvider instead of having setData be undefined until we have dataProvider?
/** Send an update to the backend to update the data. Let the update handle actually updating our data here */
const setData = useMemo(
() =>
dataProviderInfo && !isDisposed
? async (newData: TSetData) =>
dataProviderInfo.dataProvider.set(selector, newData)
dataProvider && !isDisposed
? async (newData: TSetData) => dataProvider.set(selector, newData)
: undefined,
[dataProviderInfo, selector, isDisposed],
[dataProvider, selector, isDisposed],
);

return [data, setData, isLoading];
Expand Down
19 changes: 19 additions & 0 deletions src/renderer/testing/TestButtonsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { WebView, WebViewProps } from '@renderer/components/WebView';
import useEvent from '@renderer/hooks/papi-hooks/useEvent';
import { AddWebViewEvent } from '@shared/services/WebViewService';
import useData from '@renderer/hooks/papi-hooks/useData';
import useDataProvider from '@renderer/hooks/papi-hooks/useDataProvider';

const testBase: (message: string) => Promise<string> =
NetworkService.createRequestFunction('electronAPI.env.test');
Expand Down Expand Up @@ -195,6 +196,24 @@ function TestButtonsPanel() {
[setVerseRefDebounced],
);

// Test a method on a data provider engine that isn't on the interface to see if you can actually do this
const [hasTestedRandomMethod, setHasTestedRandomMethod] = useState(false);
const [greetingsDataProvider] = useDataProvider<
string,
string,
string | { text: string; heresy: boolean }
>('hello-someone.greetings');
if (!hasTestedRandomMethod && greetingsDataProvider)
greetingsDataProvider
// @ts-ignore ts(2339)
.testRandomMethod('from test buttons panel')
.then((result: string) => {
setHasTestedRandomMethod(true);
logger.log(result);
return result;
})
.catch(logger.error);

const [verseText, setVerseText, verseTextIsLoading] = useData<
string,
string,
Expand Down
1 change: 0 additions & 1 deletion src/shared/models/IDataProviderEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export type DataProviderListenerUpdate<TData> =
* @type `TGetData` - the type of data provided by this data provider when you run `get` based on a provided selector
* @type `TSetData` - the type of data ingested by this data provider when you run `set` based on a provided selector
*/
// TODO: fix this interface's usage in DataProviderService so you can use arrow functions? https://stackoverflow.com/questions/35686850/determine-if-a-javascript-function-is-a-bound-function
interface IDataProviderEngine<TSelector, TGetData, TSetData> {
/**
* Method to run to send clients updates outside of the `set` method.
Expand Down
7 changes: 4 additions & 3 deletions src/shared/services/DataProviderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,15 @@ function buildDataProvider<TSelector, TGetData, TSetData>(
networkObject: undefined,
};

/** Saved bound version of the data provider engine's set so we can call it from here */
const dpeSet = dataProviderEngine.set.bind(dataProviderEngine);

// Object whose methods to run first when the data provider's method is called if they exist here
// before falling back to the dataProviderEngine's methods
const dataProviderInternal: IDataProvider<TSelector, TGetData, TSetData> = {
/** Layered set that emits an update event after running the engine's set */
set: async (selector, data) => {
const dpeSetResult = await dataProviderEngine.set.bind(
dataProviderEngine,
)(selector, data);
const dpeSetResult = await dpeSet(selector, data);
if (dpeSetResult) onDidUpdateEmitter.emit(dpeSetResult);
return dpeSetResult;
},
Expand Down
1 change: 0 additions & 1 deletion src/shared/services/NetworkObjectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ const set = async <T extends NetworkableObject>(
NetworkService.registerRequestHandler(
buildNetworkObjectRequestType(id, NetworkObjectRequestSubtype.Function),
(functionName: string, ...args: unknown[]) =>
// TODO: try to figure out binding the function to the object if the function is not already bound so you can use class methods? https://stackoverflow.com/questions/35686850/determine-if-a-javascript-function-is-a-bound-function
// Took the indexing off of NetworkableObject so normal objects could be used,
// but now members can't be accessed by indexing in NetworkObjectService
// TODO: fix it so it is indexable but can have specific members
Expand Down

0 comments on commit b4e4284

Please sign in to comment.