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

Async settings service #752

Merged
merged 25 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
00d132b
initial commit
rolfheij Feb 5, 2024
8ed04f3
format service, build model
Feb 5, 2024
c823490
Merge branch 'main' of github.com:paranext/paranext-core into 502-asy…
Feb 5, 2024
abebb9a
data provider types, adding settings info data
Feb 6, 2024
b8af80a
Merge branch 'main' into 502-async-settings-service
rolfheij Feb 6, 2024
4f8d764
Updates to settings service and useSetting hook
rolfheij-sil Feb 6, 2024
4bc18de
Update papi
rolfheij-sil Feb 6, 2024
1da3c05
Update use setting hook
rolfheij-sil Feb 6, 2024
2439c5a
Processed review comments. Add start service
rolfheij-sil Feb 7, 2024
9ef5ab8
Add loading settings from file
rolfheij-sil Feb 7, 2024
fb9ba84
Process review comments
rolfheij-sil Feb 8, 2024
5340485
Merge branch 'main' into 502-async-settings-service
rolfheij-sil Feb 8, 2024
f56e5d0
Merge branch 'main' into 502-async-settings-service
rolfheij-sil Feb 9, 2024
6633969
Process more review comments
rolfheij-sil Feb 9, 2024
388a369
Remove TODOs
rolfheij-sil Feb 9, 2024
3d48b47
fix unit test bug, start settings service unit tests, and small fix i…
Feb 12, 2024
f4dc247
Working on getter, setter and reset
rolfheij-sil Feb 12, 2024
b50eea5
fix reset, work on unit tests
Feb 12, 2024
389cd2d
Merge branch 'main' into 502-async-settings-service
rolfheij-sil Feb 13, 2024
91290ab
Update service host
rolfheij-sil Feb 13, 2024
4baf9a1
finish unit tests
Feb 13, 2024
dcda496
Processed some more review comments
rolfheij-sil Feb 13, 2024
7c0d51d
Add beforeEach function for tests, and deep clone data passed into da…
rolfheij-sil Feb 13, 2024
1489050
Small update to settings service host
rolfheij-sil Feb 14, 2024
fc982ca
Merge branch 'main' into 502-async-settings-service
rolfheij-sil Feb 14, 2024
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
213 changes: 121 additions & 92 deletions lib/papi-dts/papi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2165,7 +2165,7 @@ declare module 'papi-shared-types' {
type CommandNames = keyof CommandHandlers;
interface SettingTypes {
'platform.verseRef': ScriptureReference;
placeholder: undefined;
'platform.interfaceLanguage': string;
}
type SettingNames = keyof SettingTypes;
/** This is just a simple example so we have more than one. It's not intended to be real. */
Expand Down Expand Up @@ -3738,76 +3738,6 @@ declare module 'renderer/hooks/papi-hooks/use-dialog-callback.hook' {
): (optionOverrides?: Partial<DialogOptions & UseDialogCallbackOptions>) => Promise<void>;
export default useDialogCallback;
}
declare module 'shared/services/settings.service' {
import { Unsubscriber } from 'platform-bible-utils';
import { SettingNames, SettingTypes } from 'papi-shared-types';
/** Event to set or update a setting */
export type UpdateSettingEvent<SettingName extends SettingNames> = {
type: 'update-setting';
setting: SettingTypes[SettingName];
};
/** Event to remove a setting */
export type ResetSettingEvent = {
type: 'reset-setting';
};
/** All supported setting events */
export type SettingEvent<SettingName extends SettingNames> =
| UpdateSettingEvent<SettingName>
| ResetSettingEvent;
/**
* Retrieves the value of the specified setting
*
* @param key The string id of the setting for which the value is being retrieved
* @param defaultSetting The default value used for the setting if no value is available for the key
* @returns The value of the specified setting, parsed to an object. Returns default setting if
* setting does not exist
*/
const getSetting: <SettingName extends keyof SettingTypes>(
key: SettingName,
defaultSetting: SettingTypes[SettingName],
) => SettingTypes[SettingName];
/**
* Sets the value of the specified setting
*
* @param key The string id of the setting for which the value is being retrieved
* @param newSetting The value that is to be stored. Setting the new value to `undefined` is the
* equivalent of deleting the setting
*/
const setSetting: <SettingName extends keyof SettingTypes>(
key: SettingName,
newSetting: SettingTypes[SettingName],
) => void;
/**
* Removes the setting from memory
*
* @param key The string id of the setting for which the value is being removed
*/
const resetSetting: <SettingName extends keyof SettingTypes>(key: SettingName) => void;
/**
* Subscribes to updates of the specified setting. Whenever the value of the setting changes, the
* callback function is executed.
*
* @param key The string id of the setting for which the value is being subscribed to
* @param callback The function that will be called whenever the specified setting is updated
* @returns Unsubscriber that should be called whenever the subscription should be deleted
*/
const subscribeToSetting: <SettingName extends keyof SettingTypes>(
key: SettingName,
callback: (newSetting: SettingEvent<SettingName>) => void,
) => Unsubscriber;
export interface SettingsService {
get: typeof getSetting;
set: typeof setSetting;
reset: typeof resetSetting;
subscribe: typeof subscribeToSetting;
}
/**
*
* Service that allows to get and set settings in local storage
*/
const settingsService: SettingsService;
export default settingsService;
}
declare module '@papi/core' {
/** Exporting empty object so people don't have to put 'type' in their import statements */
const core: {};
Expand Down Expand Up @@ -3839,7 +3769,6 @@ declare module '@papi/core' {
WebViewProps,
} from 'shared/models/web-view.model';
export type { IWebViewProvider } from 'shared/models/web-view-provider.model';
export type { SettingEvent } from 'shared/services/settings.service';
}
declare module 'shared/services/menu-data.service-model' {
import {
Expand Down Expand Up @@ -3963,6 +3892,102 @@ declare module 'shared/services/menu-data.service' {
const menuDataService: IMenuDataService;
export default menuDataService;
}
declare module 'shared/services/settings.service-model' {
import { SettingNames, SettingTypes } from 'papi-shared-types';
import { OnDidDispose, UnsubscriberAsync } from 'platform-bible-utils';
import {
DataProviderSubscriberOptions,
DataProviderUpdateInstructions,
IDataProvider,
} from '@papi/core';
/**
*
* This name is used to register the settings service data provider on the papi. You can use this
* name to find the data provider when accessing it using the useData hook
*/
export const settingsServiceDataProviderName = 'platform.settingsServiceDataProvider';
export const settingsServiceObjectToProxy: Readonly<{
/**
*
* This name is used to register the settings service data provider on the papi. You can use this
* name to find the data provider when accessing it using the useData hook
*/
dataProviderName: 'platform.settingsServiceDataProvider';
}>;
/**
* SettingDataTypes handles getting and setting Platform.Bible core application and extension
* settings.
*
* Note: the unnamed (`''`) data type is not actually part of `SettingDataTypes` because the methods
* would not be able to create a generic type extending from `SettingNames` in order to return the
* specific setting type being requested. As such, `get`, `set`, `reset` and `subscribe` are all
* specified on {@link ISettingsService} instead. Unfortunately, as a result, using Intellisense with
* `useData` will not show the unnamed data type (`''`) as an option, but you can use `useSetting`
* instead. However, do note that the unnamed data type (`''`) is fully functional.
*/
export type SettingDataTypes = {};
export type AllSettingsData = {
[SettingName in SettingNames]: SettingTypes[SettingName];
};
module 'papi-shared-types' {
interface DataProviders {
[settingsServiceDataProviderName]: ISettingsService;
}
}
/** */
export type ISettingsService = {
/**
* Retrieves the value of the specified setting
*
* @param key The string id of the setting for which the value is being retrieved
* @returns The value of the specified setting, parsed to an object. Returns default setting if
* setting does not exist
* @throws If no default value is available for the setting.
*/
get<SettingName extends SettingNames>(key: SettingName): Promise<SettingTypes[SettingName]>;
/**
* Sets the value of the specified setting
*
* @param key The string id of the setting for which the value is being set
* @param newSetting The value that is to be set for the specified key
* @returns Information that papi uses to interpret whether to send out updates. Defaults to
* `true` (meaning send updates only for this data type).
* @see {@link DataProviderUpdateInstructions} for more info on what to return
*/
set<SettingName extends SettingNames>(
key: SettingName,
newSetting: SettingTypes[SettingName],
): Promise<DataProviderUpdateInstructions<SettingDataTypes>>;
/**
* Removes the setting from memory and resets it to its default value
*
* @param key The string id of the setting for which the value is being removed
* @returns `true` if successfully reset the project setting. `false` otherwise
*/
reset<SettingName extends SettingNames>(key: SettingName): Promise<boolean>;
/**
* Subscribes to updates of the specified setting. Whenever the value of the setting changes, the
* callback function is executed.
*
* @param key The string id of the setting for which the value is being subscribed to
* @param callback The function that will be called whenever the specified setting is updated
* @param options Various options to adjust how the subscriber emits updates
* @returns Unsubscriber that should be called whenever the subscription should be deleted
*/
subscribe<SettingName extends SettingNames>(
key: SettingName,
callback: (newSetting: SettingTypes[SettingName]) => void,
options?: DataProviderSubscriberOptions,
): Promise<UnsubscriberAsync>;
} & OnDidDispose &
IDataProvider<SettingDataTypes> &
typeof settingsServiceObjectToProxy;
}
declare module 'shared/services/settings.service' {
import { ISettingsService } from 'shared/services/settings.service-model';
const settingsService: ISettingsService;
export default settingsService;
}
declare module '@papi/backend' {
/**
* Unified module for accessing API features in the extension host.
Expand All @@ -3983,6 +4008,7 @@ declare module '@papi/backend' {
import { ProjectLookupServiceType } from 'shared/services/project-lookup.service-model';
import { DialogService } from 'shared/services/dialog.service-model';
import { IMenuDataService } from 'shared/services/menu-data.service-model';
import { ISettingsService } from 'shared/services/settings.service-model';
const papi: {
/**
*
Expand Down Expand Up @@ -4059,6 +4085,8 @@ declare module '@papi/backend' {
* within the renderer.
*/
storage: ExtensionStorageService;
/** */
settings: ISettingsService;
/**
*
* Service that allows to get and store menu data
Expand Down Expand Up @@ -4426,23 +4454,26 @@ declare module 'renderer/hooks/papi-hooks/use-data.hook' {
}
declare module 'renderer/hooks/papi-hooks/use-setting.hook' {
import { SettingTypes } from 'papi-shared-types';
import {
DataProviderSubscriberOptions,
DataProviderUpdateInstructions,
} from 'shared/models/data-provider.model';
import { SettingDataTypes } from 'shared/services/settings.service-model';
/**
* Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes
* and gets updated when the setting is changed by others.
*
* @param key The string id that is used to store the setting in local storage
* @param key The string id that is used to identify the setting that will be stored on the papi
*
* WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be
* updated every render
* @param defaultState The default state of the setting. If the setting already has a value set to
* it in local storage, this parameter will be ignored.
* @param defaultState The initial value to return while first awaiting the setting value
* @param subscriberOptions Various options to adjust how the subscriber emits updates
*
* Note: this parameter is internally assigned to a `ref`, so changing it will not cause any hooks
* to re-run with its new value. Running `resetSetting()` will always update the setting value
* returned to the latest `defaultState`, and changing the `key` will use the latest
* `defaultState`. However, if `defaultState` is changed while a setting is `defaultState`
* (meaning it is reset and has no value), the returned setting value will not be updated to the
* new `defaultState`.
* to re-run with its new value. This means that `subscriberOptions` will be passed to the data
* provider's `subscribe<data_type>` method as soon as possible and will not be updated again
* until `dataProviderSource` or `selector` changes.
* @returns `[setting, setSetting, resetSetting]`
*
* - `setting`: The current state of the setting, either `defaultState` or the stored state on the
Expand All @@ -4456,10 +4487,14 @@ declare module 'renderer/hooks/papi-hooks/use-setting.hook' {
const useSetting: <SettingName extends keyof SettingTypes>(
key: SettingName,
defaultState: SettingTypes[SettingName],
subscriberOptions?: DataProviderSubscriberOptions,
) => [
setting: SettingTypes[SettingName],
setSetting: (newSetting: SettingTypes[SettingName]) => void,
setSetting: (
newData: SettingTypes[SettingName],
) => Promise<DataProviderUpdateInstructions<SettingDataTypes>>,
resetSetting: () => void,
isLoading: boolean,
];
export default useSetting;
}
Expand Down Expand Up @@ -4709,7 +4744,7 @@ declare module '@papi/frontend' {
import { DataProviderService } from 'shared/services/data-provider.service';
import { ProjectLookupServiceType } from 'shared/services/project-lookup.service-model';
import { PapiFrontendProjectDataProviderService } from 'shared/services/project-data-provider.service';
import { SettingsService } from 'shared/services/settings.service';
import { ISettingsService } from 'shared/services/settings.service-model';
import { DialogService } from 'shared/services/dialog.service-model';
import * as papiReact from '@papi/frontend/react';
import PapiRendererWebSocket from 'renderer/services/renderer-web-socket.service';
Expand Down Expand Up @@ -4789,11 +4824,8 @@ declare module '@papi/frontend' {
* React hooks that enable interacting with the `papi` in React components more easily.
*/
react: typeof papiReact;
/**
*
* Service that allows to get and set settings in local storage
*/
settings: SettingsService;
/** */
settings: ISettingsService;
/**
*
* Service that allows to get and store menu data
Expand Down Expand Up @@ -4874,11 +4906,8 @@ declare module '@papi/frontend' {
* React hooks that enable interacting with the `papi` in React components more easily.
*/
export const react: typeof papiReact;
/**
*
* Service that allows to get and set settings in local storage
*/
export const settings: SettingsService;
/** */
export const settings: ISettingsService;
/**
*
* Service that allows to get and store menu data
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/app.component.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ jest.mock('@renderer/components/docking/platform-dock-layout.component', () => (
__esModule: true,
default: /** ParanextDockLayout Mock */ () => undefined,
}));
jest.mock('@renderer/components/platform-bible-toolbar', () => ({
__esModule: true,
default: /** PlatformBibleToolbar Mock */ () => <div />,
}));

describe('App', () => {
it('should render', async () => {
Expand Down
5 changes: 1 addition & 4 deletions src/declarations/papi-shared-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,7 @@ declare module 'papi-shared-types' {

export interface SettingTypes {
'platform.verseRef': ScriptureReference;
// With only one key in this interface, `papi.d.ts` was baking in the literal string when
// `SettingNames` was being used. Adding a placeholder key makes TypeScript generate `papi.d.ts`
// correctly. When we add another setting, we can remove this placeholder.
placeholder: undefined;
'platform.interfaceLanguage': string;
}

export type SettingNames = keyof SettingTypes;
Expand Down
4 changes: 4 additions & 0 deletions src/extension-host/services/papi-backend.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import dialogService from '@shared/services/dialog.service';
import { DialogService } from '@shared/services/dialog.service-model';
import menuDataService from '@shared/services/menu-data.service';
import { IMenuDataService } from '@shared/services/menu-data.service-model';
import settingsService from '@shared/services/settings.service';
import { ISettingsService } from '@shared/services/settings.service-model';

// IMPORTANT NOTES:
// 1) When adding new services here, consider whether they also belong in papi-frontend.service.ts.
Expand Down Expand Up @@ -73,6 +75,8 @@ const papi = {
projectLookup: projectLookupService as ProjectLookupServiceType,
/** JSDOC DESTINATION extensionStorageService */
storage: extensionStorageService as ExtensionStorageService,
/** JSDOC DESTINATION settingsService */
settings: settingsService as ISettingsService,
/** JSDOC DESTINATION menuDataService */
menuData: menuDataService as IMenuDataService,
};
Expand Down
19 changes: 19 additions & 0 deletions src/main/data/core-settings-info.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SettingNames, SettingTypes } from 'papi-shared-types';

/** Information about one setting */
type SettingInfo<SettingName extends SettingNames> = {
default: SettingTypes[SettingName];
};

/** Information about all settings. Keys are setting keys, values are information for that setting */
export type AllSettingsInfo = {
[SettingName in SettingNames]: SettingInfo<SettingName>;
};

/** Info about all settings built into core. Does not contain info for extensions' settings */
const coreSettingsInfo: Partial<AllSettingsInfo> = {
'platform.verseRef': { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } },
'platform.interfaceLanguage': { default: 'eng' },
};

export default coreSettingsInfo;
3 changes: 3 additions & 0 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { get } from '@shared/services/project-data-provider.service';
import { VerseRef } from '@sillsdev/scripture';
import { startNetworkObjectStatusService } from './services/network-object-status.service-host';
import { startLocalizationService } from './services/localization.service-host';
import { initialize as initializeSettingsService } from './services/settings.service-host';

const PROCESS_CLOSE_TIME_OUT = 2000;

Expand Down Expand Up @@ -82,6 +83,8 @@ async function main() {

await startLocalizationService();

await initializeSettingsService();

// TODO (maybe): Wait for signal from the extension host process that it is ready (except 'getWebView')
// We could then wait for the renderer to be ready and signal the extension host

Expand Down
Loading
Loading