diff --git a/cspell.json b/cspell.json index 6fe9cc4d3c..39fa1cfb91 100644 --- a/cspell.json +++ b/cspell.json @@ -37,6 +37,7 @@ "papis", "paranext", "paratext", + "PDPE", "pdpf", "pdps", "plusplus", diff --git a/extensions/src/hello-world/src/types/hello-world.d.ts b/extensions/src/hello-world/src/types/hello-world.d.ts index dee31ab034..9f6a35ce44 100644 --- a/extensions/src/hello-world/src/types/hello-world.d.ts +++ b/extensions/src/hello-world/src/types/hello-world.d.ts @@ -1,7 +1,7 @@ declare module 'hello-world' { - import type { DataProviderDataType, IDataProvider, MandatoryProjectDataType } from '@papi/core'; + import type { DataProviderDataType, IDataProvider, MandatoryProjectDataTypes } from '@papi/core'; - export type MyProjectDataType = MandatoryProjectDataType & { + export type MyProjectDataType = MandatoryProjectDataTypes & { MyProjectData: DataProviderDataType; }; @@ -23,6 +23,7 @@ declare module 'hello-world' { declare module 'papi-shared-types' { import type { MyProjectDataProvider } from 'hello-world'; + import type { IProjectStorageInterpreter } from 'papi-shared-types'; export interface CommandHandlers { 'helloWorld.helloWorld': () => string; @@ -32,4 +33,9 @@ declare module 'papi-shared-types' { export interface ProjectDataProviders { 'helloWorld.myExtensionProjectTypeName': MyProjectDataProvider; } + + export interface ProjectStorageInterpreters { + /** Placeholder. Implementation TBD */ + 'helloWorld.myExtensionProjectTypeName': IProjectStorageInterpreter; + } } diff --git a/extensions/src/usfm-data-provider/index.d.ts b/extensions/src/usfm-data-provider/index.d.ts index 6f18832383..3f2d5a68b9 100644 --- a/extensions/src/usfm-data-provider/index.d.ts +++ b/extensions/src/usfm-data-provider/index.d.ts @@ -6,7 +6,7 @@ declare module 'usfm-data-provider' { DataProviderUpdateInstructions, ExtensionDataScope, IDataProvider, - MandatoryProjectDataType, + MandatoryProjectDataTypes, } from '@papi/core'; import { UnsubscriberAsync } from 'platform-bible-utils'; @@ -25,7 +25,7 @@ declare module 'usfm-data-provider' { * * This is not yet a complete list of the data types available from Paratext projects. */ - export type ParatextStandardProjectDataTypes = MandatoryProjectDataType & { + export type ParatextStandardProjectDataTypes = MandatoryProjectDataTypes & { /** Gets/sets the "raw" USFM data for the specified book */ BookUSFM: DataProviderDataType; /** Gets/sets the "raw" USFM data for the specified chapter */ @@ -364,12 +364,40 @@ declare module 'usfm-data-provider' { declare module 'papi-shared-types' { import type { ParatextStandardProjectDataProvider, UsfmDataProvider } from 'usfm-data-provider'; + import type { IProjectStorageInterpreter } from 'papi-shared-types'; export interface ProjectDataProviders { ParatextStandard: ParatextStandardProjectDataProvider; } + export interface ProjectStorageInterpreters { + /** Placeholder. Implementation TBD */ + ParatextStandard: IProjectStorageInterpreter; + } + export interface DataProviders { usfm: UsfmDataProvider; } + + export interface ProjectSettingTypes { + /** + * Which versification scheme this Scripture project uses + * + * WARNING: This setting is an example and needs to be updated to support proper versification + * specification! For the moment, it simply corresponds to + * [`ScrVersType`](https://github.com/sillsdev/libpalaso/blob/master/SIL.Scripture/Versification.cs#L1340), + * but this is not correct and full design. + */ + 'platformScripture.versification': number; + /** + * Which books are present in this Scripture project. Represented as a string with 0 or 1 for + * each possible book by [standardized book + * code](https://github.com/sillsdev/libpalaso/blob/master/SIL.Scripture/Canon.cs#L226) (123 + * characters long) + * + * @example + * '100111000000000000110000001000000000010111111111111111111111111111000000000000000000000000000000000000000000100000000000000' + */ + 'platformScripture.booksPresent': string; + } } diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index 669e3ddb08..32a49ecd55 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -1573,20 +1573,20 @@ declare module 'shared/models/network-object.model' { * call that method. This is because we don't want users of network objects to dispose of them. Only * the caller of `networkObjectService.set` should be able to dispose of the network object. * - * @see networkObjectService + * @see {@link networkObjectService} */ export type NetworkObject = Omit, 'dispose'> & OnDidDispose; /** * An object of this type is returned from {@link networkObjectService.set}. * - * @see networkObjectService + * @see {@link networkObjectService} */ export type DisposableNetworkObject = NetworkObject & Dispose; /** * An object of this type is passed into {@link networkObjectService.set}. * - * @see networkObjectService + * @see {@link networkObjectService} */ export type NetworkableObject = T & CannotHaveOnDidDispose; /** @@ -1608,7 +1608,7 @@ declare module 'shared/models/network-object.model' { * Note: This function should return Partial. For some reason, TypeScript can't infer the type * (probably has to do with that it's a wrapped and layered type). Functions that implement this * type should return Partial - * @see networkObjectService + * @see {@link networkObjectService} */ export type LocalObjectToProxyCreator = ( id: string, @@ -1697,7 +1697,7 @@ declare module 'shared/models/data-provider.model' { * @param data The data that determines what to set at the selector * @returns Information that papi uses to interpret whether to send out updates. Defaults to `true` * (meaning send updates only for this data type). - * @see DataProviderUpdateInstructions for more info on what to return + * @see {@link DataProviderUpdateInstructions} for more info on what to return */ export type DataProviderSetter< TDataTypes extends DataProviderDataTypes, @@ -1798,14 +1798,14 @@ declare module 'shared/models/data-provider.model' { * Names of data types in a DataProviderDataTypes type. Indicates the data types that a data * provider can handle (so it will have methods with these names like `set`) * - * @see DataProviderDataTypes for more information + * @see {@link DataProviderDataTypes} for more information */ export type DataTypeNames = keyof TDataTypes & string; /** * Set of all `set` methods that a data provider provides according to its data types. * - * @see DataProviderSetter for more information + * @see {@link DataProviderSetter} for more information */ export type DataProviderSetters = { [DataType in keyof TDataTypes as `set${DataType & string}`]: DataProviderSetter< @@ -1816,7 +1816,7 @@ declare module 'shared/models/data-provider.model' { /** * Set of all `get` methods that a data provider provides according to its data types. * - * @see DataProviderGetter for more information + * @see {@link DataProviderGetter} for more information */ export type DataProviderGetters = { [DataType in keyof TDataTypes as `get${DataType & string}`]: DataProviderGetter< @@ -1827,7 +1827,7 @@ declare module 'shared/models/data-provider.model' { * Set of all `subscribe` methods that a data provider provides according to its data * types. * - * @see DataProviderSubscriber for more information + * @see {@link DataProviderSubscriber} for more information */ export type DataProviderSubscribers = { [DataType in keyof TDataTypes as `subscribe${DataType & string}`]: DataProviderSubscriber< @@ -1839,7 +1839,7 @@ declare module 'shared/models/data-provider.model' { * object layers over the data provider engine and runs its methods along with other methods. This * object is transformed into an IDataProvider by networkObjectService.set. * - * @see IDataProvider + * @see {@link IDataProvider} */ type DataProviderInternal = NetworkableObject< @@ -1859,13 +1859,17 @@ declare module 'shared/models/data-provider.model' { export default DataProviderInternal; } declare module 'shared/models/project-data-provider.model' { - import type { DataProviderDataType } from 'shared/models/data-provider.model'; + import type { + DataProviderDataType, + DataProviderDataTypes, + DataProviderUpdateInstructions, + } from 'shared/models/data-provider.model'; /** Indicates to a PDP what extension data is being referenced */ export type ExtensionDataScope = { /** Name of an extension as provided in its manifest */ extensionName: string; /** - * Name of a unique partition or segment of data within the extension Some examples include (but + * Name of a unique partition or segment of data within the extension. Some examples include (but * are not limited to): * * - Name of an important data structure that is maintained in a project @@ -1873,18 +1877,53 @@ declare module 'shared/models/project-data-provider.model' { * - Name of a resource created by a user that should be maintained in a project * * This is the smallest level of granularity provided by a PDP for accessing extension data. There - * is no way to get or set just a portion of data identified by a single dataQualifier value. + * is no way to get or set just a portion of data identified by a single `dataQualifier` value. */ dataQualifier: string; }; /** + * `DataProviderDataTypes` that each project data provider **must** implement. They are assumed to + * exist and are used by project storage interpreters and other data providers + * + * --- + * + * ### Setting + * + * The `Setting` data type handles getting and setting project settings. All Project Data Providers + * must implement these methods `getSetting` and `setSetting` as well as `resetSetting` in order to + * properly support project settings. In most cases, the Project Data Provider only needs to pass + * the setting calls through to the Project Storage Interpreter. + * + * Note: the `Setting` data type is not actually part of {@link MandatoryProjectDataTypes} because + * the methods would not be able to create a generic type extending from `ProjectSettingNames` in + * order to return the specific setting type being requested. As such, `getSetting`, `setSetting`, + * and `subscribeSetting` are all specified on {@link IProjectDataProvider} instead. Unfortunately, + * as a result, using Intellisense with `useProjectData` will not show `Setting` as a data type + * option, but you can use `useProjectSetting` instead. However, do note that the `Setting` data + * type is fully functional. + * + * The closest possible representation of the `Setting` data type follows: + * + * ```typescript + * Setting: DataProviderDataType< + * ProjectSettingNames, + * ProjectSettingTypes[ProjectSettingNames], + * ProjectSettingTypes[ProjectSettingNames] + * >; + * ``` + * + * --- + * + * ### ExtensionData + * * All Project Data Provider data types must have an `ExtensionData` type. We strongly recommend all * Project Data Provider data types extend from this type in order to standardize the * `ExtensionData` types. * * Benefits of following this standard: * - * - All PSIs that support this `projectType` can use a standardized `ExtensionData` interface + * - All project storage interpreters that support this `projectType` can use a standardized + * `ExtensionData` interface * - If an extension uses the `ExtensionData` endpoint for any project, it will likely use this * standardized interface, so using this interface on your Project Data Provider data types * enables your PDP to support generic extension data @@ -1892,9 +1931,174 @@ declare module 'shared/models/project-data-provider.model' { * so following this interface ensures your PDP will not break if such a requirement is * implemented. */ - export type MandatoryProjectDataType = { + export type MandatoryProjectDataTypes = { ExtensionData: DataProviderDataType; }; + /** + * The `ExtensionData` methods required for a Project Data Provider Engine to fulfill the + * requirements of {@link MandatoryProjectDataTypes}'s `ExtensionData` data type. + * + * Note: These methods are already covered by {@link MandatoryProjectDataTypes}, but this type adds + * JSDocs for them. + */ + export type WithProjectDataProviderEngineExtensionDataMethods< + TProjectDataTypes extends DataProviderDataTypes, + > = { + /** + * Gets an extension's project data identified by `dataScope` in this project + * + * @param dataScope Information about what data is being referenced by the calling extension given + * to this Project Data Provider + * @returns Extension project data in this project for an extension to use in serving its + * extension project data + */ + getExtensionData(dataScope: ExtensionDataScope): Promise; + /** + * Sets an extension's project data identified by `dataScope` in this project + * + * @param dataScope Information about what data is being referenced by the calling extension given + * to this Project Data Provider + * @param data Updated value of extension project data in this project to set + * @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 + */ + setExtensionData( + dataScope: ExtensionDataScope, + data: string, + ): Promise>; + }; +} +declare module 'shared/models/project-storage-interpreter.model' { + import { DataProviderDataType } from 'shared/models/data-provider.model'; + import { ExtensionDataScope } from 'shared/models/project-data-provider.model'; + /** Indicates to a PSI what raw project data chunk is being referenced */ + export type ProjectStorageProjectDataScope = { + /** ID for the project whose raw data chunk to get */ + projectId: string; + /** + * Name of a unique partition or segment of data within the project. Some examples include (but + * are not limited to): + * + * - Name of an important data structure that is maintained in a project + * - Name of a downloaded data set that is being cached + * - Name of a resource created by a user that should be maintained in a project + * + * This is the smallest level of granularity provided by a PSI for accessing raw project data. + * There is no way to get or set just a portion of data identified by a single `dataQualifier` + * value. + */ + dataQualifier: string; + }; + /** + * `DataProviderDataTypes` that are a sensible default for project storage interpreters to + * implement. Using {@link IProjectStorageInterpreter} without specifying data types will default to + * these data types. These types are simply a recommendation for how to write a PSI for a specified + * `projectType`. As long as both the Project Data Provider and the Project Storage Interpreter for + * a given `projectType` communicate with the same interface, you are free to design the + * communication in the way that makes most sense for the `projectType`. + * + * Note: Project Data Providers are associated to Project Storage Interpreters based on a shared + * `projectType`. A PSI must implement the data types specified for each `projectType` it supports. + * + * --- + * + * ### ProjectData + * + * A simple data type for a Project Data Provider to use to retrieve raw data chunks for a specific + * project from a Project Storage Interpreter with the same `projectType`. The Project Data Provider + * indicates which project it is associated with and specifies the name of a segment of data within + * the project. + * + * Benefits of following this recommendation: + * + * - Serving raw data chunks according to a simple specifier keeps the Project Storage Interpreter + * thin and simple so multiple thin PSIs can be made for different `storageType`s while leaving + * the complex task of parsing and serving project data to the Project Data Provider. + * - This is an easy pattern to follow when starting to learn how to make new `projectType`s in + * Platform.Bible. + */ + export type DefaultProjectStorageDataTypes = { + ProjectData: DataProviderDataType; + }; + /** + * Indicates to a PSI what extension data is being referenced on what project. Generally, a PDP + * passes calls to `ExtensionData` data type methods to its PSI and adds the `projectId`. + */ + export type ProjectStorageExtensionDataScope = ExtensionDataScope & { + /** ID for the project whose extension data to get */ + projectId: string; + }; + /** + * `DataProviderDataTypes` that each project storage interpreter must implement. They are assumed to + * exist and are used by project data providers + * + * --- + * + * ### Setting + * + * The `Setting` data type handles getting and setting project settings. All Project Storage + * Interpreters must implement these methods `getSetting` and `setSetting` as well as `resetSetting` + * in order to properly support project settings. In most cases, the Project Data Provider will pass + * `Setting` calls through to the Project Storage Interpreter. + * + * Note: the `Setting` data type is not actually part of {@link MandatoryProjectStorageDataTypes} + * because the methods would not be able to create a generic type extending from + * `ProjectSettingNames` in order to return the specific setting type being requested. As such, + * `getSetting`, `setSetting`, and `subscribeSetting` are all specified on + * {@link IProjectStorageInterpreter} instead. However, do note that the `Setting` data type is fully + * functional. + * + * The closest possible representation of the `Setting` data type follows: + * + * ```typescript + * Setting: DataProviderDataType< + * ProjectStorageSettingDataScope, + * ProjectSettingTypes[ProjectSettingNames], + * ProjectSettingTypes[ProjectSettingNames] + * >; + * ``` + * + * WARNING: Each Project Storage Interpreter **needs** to fulfill the following requirements for its + * settings-related methods: + * + * - `getSetting`: if a project setting value is present for the key requested, return it. Otherwise, + * you must call `papi.projectSettings.getDefault` to get the default value or throw if that call + * throws. This functionality preserves the intended type of the setting and avoids returning + * `undefined` unexpectedly. + * - `setSetting`: must call `papi.projectSettings.isValid` before setting the value and should return + * false if the call returns `false`. This functionality preserves the intended intended type of + * the setting and avoids allowing the setting to be set to the wrong type. + * - `resetSetting`: deletes the value at the key and sends a setting update event. After this, + * `getSetting` should see the setting value as not present and return the default value again. + * - Note: see {@link IProjectStorageInterpreter} for method signatures for these three methods. + * + * .--- + * + * ### ExtensionData + * + * All Project Storage Interpreter data types must have an `ExtensionData` type. We strongly + * recommend all Project Storage Interpreter data types extend from this type in order to + * standardize the `ExtensionData` types. Project Data Providers will call this endpoint in order to + * retrieve extensions' project data. + * + * Benefits of following this standard: + * + * - Project data providers of this `projectType` can use a standardized `ExtensionData` interface + * - If an extension uses the `ExtensionData` endpoint for any project, it will likely use this + * standardized interface, so using this interface on your Project Storage Interpreter data types + * enables your PSI to support generic extension data + * - In the future, we may enforce that callers to `ExtensionData` endpoints include `extensionName`, + * so following this interface ensures your PSI will not break if such a requirement is + * implemented. + */ + export type MandatoryProjectStorageDataTypes = { + ExtensionData: DataProviderDataType< + ProjectStorageExtensionDataScope, + string | undefined, + string + >; + }; } declare module 'shared/models/data-provider.interface' { import { @@ -1906,8 +2110,8 @@ declare module 'shared/models/data-provider.interface' { import { Dispose, OnDidDispose } from 'platform-bible-utils'; /** * An object on the papi that manages data and has methods for interacting with that data. Created - * by the papi and layers over an IDataProviderEngine provided by an extension. Returned from - * getting a data provider with dataProviderService.get. + * by the papi and layers over an {@link IDataProviderEngine} provided by an extension. Returned from + * getting a data provider with `papi.dataProviders.get`. * * Note: each `set` method has a corresponding `get` and * `subscribe` method. @@ -1923,7 +2127,7 @@ declare module 'shared/models/data-provider.interface' { * data provider (only the service that set it up should dispose of it) with * dataProviderService.registerEngine * - * @see IDataProvider + * @see {@link IDataProvider} */ export type IDisposableDataProvider> = TDataProvider & Dispose; @@ -1939,34 +2143,36 @@ declare module 'shared/models/data-provider-engine.model' { /** * * Method to run to send clients updates for a specific data type outside of the `set` - * method. papi overwrites this function on the DataProviderEngine itself to emit an update after - * running the `notifyUpdate` method in the DataProviderEngine. + * method. papi overwrites this function on the DataProviderEngine itself to emit an update based on + * the `updateInstructions` and then run the original `notifyUpdateMethod` from the + * `DataProviderEngine`. * - * @example To run `notifyUpdate` function so it updates the Verse and Heresy data types (in a data - * provider engine): + * _@example_ To run `notifyUpdate` function so it updates the Verse and Heresy data types (in a + * data provider engine): * * ```typescript * this.notifyUpdate(['Verse', 'Heresy']); * ``` * - * @example You can log the manual updates in your data provider engine by specifying the following - * `notifyUpdate` function in the data provider engine: + * _@example_ You can log the manual updates in your data provider engine by specifying the + * following `notifyUpdate` function in the data provider engine: * * ```typescript * notifyUpdate(updateInstructions) { - * papi.logger.info(updateInstructions); + * papi.logger.info(updateInstructions); * } * ``` * * Note: This function's return is treated the same as the return from `set` * - * @param updateInstructions Information that papi uses to interpret whether to send out updates. - * Defaults to `'*'` (meaning send updates for all data types) if parameter `updateInstructions` - * is not provided or is undefined. Otherwise returns `updateInstructions`. papi passes the - * interpreted update value into this `notifyUpdate` function. For example, running - * `this.notifyUpdate()` will call the data provider engine's `notifyUpdate` with - * `updateInstructions` of `'*'`. - * @see DataProviderUpdateInstructions for more info on the `updateInstructions` parameter + * _@param_ `updateInstructions` Information that papi uses to interpret whether to send out + * updates. Defaults to `'*'` (meaning send updates for all data types) if parameter + * `updateInstructions` is not provided or is undefined. Otherwise returns `updateInstructions`. + * papi passes the interpreted update value into this `notifyUpdate` function. For example, running + * `this.notifyUpdate()` will call the data provider engine's `notifyUpdate` with + * `updateInstructions` of `'*'`. + * + * _@see_ {@link DataProviderUpdateInstructions} for more info on the `updateInstructions` parameter * * WARNING: Do not update a data type in its `get` method (unless you make a base case)! * It will create a destructive infinite loop. @@ -1979,41 +2185,43 @@ declare module 'shared/models/data-provider-engine.model' { * provider engine. You do not need to specify this type unless you are creating an object that is * to be registered as a data provider engine and you need to use `notifyUpdate`. * - * @see DataProviderEngineNotifyUpdate for more information on `notifyUpdate`. - * @see IDataProviderEngine for more information on using this type. + * @see {@link DataProviderEngineNotifyUpdate} for more information on `notifyUpdate`. + * @see {@link IDataProviderEngine} for more information on using this type. */ export type WithNotifyUpdate = { /** * * Method to run to send clients updates for a specific data type outside of the `set` - * method. papi overwrites this function on the DataProviderEngine itself to emit an update after - * running the `notifyUpdate` method in the DataProviderEngine. + * method. papi overwrites this function on the DataProviderEngine itself to emit an update based on + * the `updateInstructions` and then run the original `notifyUpdateMethod` from the + * `DataProviderEngine`. * - * @example To run `notifyUpdate` function so it updates the Verse and Heresy data types (in a data - * provider engine): + * _@example_ To run `notifyUpdate` function so it updates the Verse and Heresy data types (in a + * data provider engine): * * ```typescript * this.notifyUpdate(['Verse', 'Heresy']); * ``` * - * @example You can log the manual updates in your data provider engine by specifying the following - * `notifyUpdate` function in the data provider engine: + * _@example_ You can log the manual updates in your data provider engine by specifying the + * following `notifyUpdate` function in the data provider engine: * * ```typescript * notifyUpdate(updateInstructions) { - * papi.logger.info(updateInstructions); + * papi.logger.info(updateInstructions); * } * ``` * * Note: This function's return is treated the same as the return from `set` * - * @param updateInstructions Information that papi uses to interpret whether to send out updates. - * Defaults to `'*'` (meaning send updates for all data types) if parameter `updateInstructions` - * is not provided or is undefined. Otherwise returns `updateInstructions`. papi passes the - * interpreted update value into this `notifyUpdate` function. For example, running - * `this.notifyUpdate()` will call the data provider engine's `notifyUpdate` with - * `updateInstructions` of `'*'`. - * @see DataProviderUpdateInstructions for more info on the `updateInstructions` parameter + * _@param_ `updateInstructions` Information that papi uses to interpret whether to send out + * updates. Defaults to `'*'` (meaning send updates for all data types) if parameter + * `updateInstructions` is not provided or is undefined. Otherwise returns `updateInstructions`. + * papi passes the interpreted update value into this `notifyUpdate` function. For example, running + * `this.notifyUpdate()` will call the data provider engine's `notifyUpdate` with + * `updateInstructions` of `'*'`. + * + * _@see_ {@link DataProviderUpdateInstructions} for more info on the `updateInstructions` parameter * * WARNING: Do not update a data type in its `get` method (unless you make a base case)! * It will create a destructive infinite loop. @@ -2022,13 +2230,13 @@ declare module 'shared/models/data-provider-engine.model' { }; /** * The object to register with the DataProviderService to create a data provider. The - * DataProviderService creates an IDataProvider on the papi that layers over this engine, providing - * special functionality. + * DataProviderService creates an {@link IDataProvider} on the papi that layers over this engine, + * providing special functionality. * * @type TDataTypes - The data types that this data provider engine serves. For each data type * defined, the engine must have corresponding `get` and `set function` * functions. - * @see DataProviderDataTypes for information on how to make powerful types that work well with + * @see {@link DataProviderDataTypes} for information on how to make powerful types that work well with * Intellisense. * * Note: papi creates a `notifyUpdate` function on the data provider engine if one is not provided, so it @@ -2036,8 +2244,16 @@ declare module 'shared/models/data-provider-engine.model' { * not understand that papi will create one as you are writing your data provider engine, so you can * avoid type errors with one of the following options: * - * 1. If you are using an object or class to create a data provider engine, you can add a - * `notifyUpdate` function (and, with an object, add the WithNotifyUpdate type) to + * 1. If you are using a class to create a data provider engine, you can extend the + * {@link DataProviderEngine} class, and it will provide `notifyUpdate` for you: + * ```typescript + * class MyDPE extends DataProviderEngine implements IDataProviderEngine { + * ... + * } + * ``` + * + * 2. If you are using an object or class to create a data provider engine, you can add a + * `notifyUpdate` function (and, with an object, add the {@link WithNotifyUpdate} type) to * your data provider engine like so: * ```typescript * const myDPE: IDataProviderEngine & WithNotifyUpdate = { @@ -2052,14 +2268,6 @@ declare module 'shared/models/data-provider-engine.model' { * ... * } * ``` - * - * 2. If you are using a class to create a data provider engine, you can extend the `DataProviderEngine` - * class, and it will provide `notifyUpdate` for you: - * ```typescript - * class MyDPE extends DataProviderEngine implements IDataProviderEngine { - * ... - * } - * ``` */ type IDataProviderEngine = NetworkableObject & @@ -2077,7 +2285,7 @@ declare module 'shared/models/data-provider-engine.model' { * WARNING: Do not run this recursively in its own `set` method! It will create as * many updates as you run `set` methods. * - * @see DataProviderSetter for more information + * @see {@link DataProviderSetter} for more information */ DataProviderSetters & /** @@ -2087,11 +2295,24 @@ declare module 'shared/models/data-provider-engine.model' { * Note: papi requires that each `set` method has a corresponding `get` * method. * - * @see DataProviderGetter for more information + * @see {@link DataProviderGetter} for more information */ DataProviderGetters & Partial>; export default IDataProviderEngine; + /** + * + * Abstract class that provides a placeholder `notifyUpdate` for data provider engine classes. If a + * data provider engine class extends this class, it doesn't have to specify its own `notifyUpdate` + * function in order to use `notifyUpdate`. + * + * @see {@link IDataProviderEngine} for more information on extending this class. + */ + export abstract class DataProviderEngine + implements WithNotifyUpdate + { + notifyUpdate(updateInstructions?: DataProviderUpdateInstructions): void; + } } declare module 'shared/models/extract-data-provider-data-types.model' { import IDataProviderEngine from 'shared/models/data-provider-engine.model'; @@ -2118,9 +2339,21 @@ declare module 'shared/models/extract-data-provider-data-types.model' { export default ExtractDataProviderDataTypes; } declare module 'papi-shared-types' { - import type { ScriptureReference } from 'platform-bible-utils'; - import type { DataProviderDataType } from 'shared/models/data-provider.model'; - import type { MandatoryProjectDataType } from 'shared/models/project-data-provider.model'; + import type { ScriptureReference, UnsubscriberAsync } from 'platform-bible-utils'; + import type { + DataProviderDataType, + DataProviderDataTypes, + DataProviderSubscriberOptions, + DataProviderUpdateInstructions, + } from 'shared/models/data-provider.model'; + import type { + MandatoryProjectDataTypes, + WithProjectDataProviderEngineExtensionDataMethods, + } from 'shared/models/project-data-provider.model'; + import type { + DefaultProjectStorageDataTypes, + MandatoryProjectStorageDataTypes, + } from 'shared/models/project-storage-interpreter.model'; import type { IDisposableDataProvider } from 'shared/models/data-provider.interface'; import type IDataProvider from 'shared/models/data-provider.interface'; import type ExtractDataProviderDataTypes from 'shared/models/extract-data-provider-data-types.model'; @@ -2128,7 +2361,7 @@ declare module 'papi-shared-types' { * Function types for each command available on the papi. Each extension can extend this interface * to add commands that it registers on the papi with `papi.commands.registerCommand`. * - * Note: Command names must consist of two string separated by at least one period. We recommend + * Note: Command names must consist of two strings separated by at least one period. We recommend * one period and lower camel case in case we expand the api in the future to allow dot notation. * * An extension can extend this interface to add types for the commands it registers by adding the @@ -2163,34 +2396,204 @@ declare module 'papi-shared-types' { * @example 'platform.quit'; */ type CommandNames = keyof CommandHandlers; + /** + * Types corresponding to each user setting available in Platform.Bible. Keys are setting names, + * and values are setting data types. Extensions can add more user setting types with + * corresponding user setting IDs by adding details to their `.d.ts` file. + * + * Note: Setting names must consist of two strings separated by at least one period. We recommend + * one period and lower camel case in case we expand the api in the future to allow dot notation. + * + * An extension can extend this interface to add types for the user settings it registers by + * adding the following to its `.d.ts` file (in this example, we are adding the + * `myExtension.highlightColor` setting): + * + * @example + * + * ```typescript + * declare module 'papi-shared-types' { + * export interface SettingTypes { + * 'myExtension.highlightColor': string | { r: number; g: number; b: number }; + * } + * } + * ``` + */ interface SettingTypes { 'platform.verseRef': ScriptureReference; 'platform.interfaceLanguage': string[]; } + /** + * Names for each user setting available on the papi. + * + * Automatically includes all extensions' user settings that are added to {@link SettingTypes}. + * + * @example 'platform.verseRef' + */ type SettingNames = keyof SettingTypes; + /** + * Types corresponding to each project setting available in Platform.Bible. Keys are project + * setting names, and values are project setting data types. Extensions can add more project + * setting types with corresponding project setting IDs by adding details to their `.d.ts` file. + * + * Note: Project setting names must consist of two strings separated by at least one period. We + * recommend one period and lower camel case in case we expand the api in the future to allow dot + * notation. + * + * An extension can extend this interface to add types for the project settings it registers by + * adding the following to its `.d.ts` file (in this example, we are adding the + * `myExtension.highlightColor` project setting): + * + * @example + * + * ```typescript + * declare module 'papi-shared-types' { + * export interface ProjectSettingTypes { + * 'myExtension.highlightColor': string | { r: number; g: number; b: number }; + * } + * } + * ``` + */ + interface ProjectSettingTypes { + /** + * Localized name of the language in which this project is written. This will be displayed + * directly in the UI. + * + * @example 'English' + */ + 'platform.language': string; + /** + * Localized full name of the project. This will be displayed directly in the UI. + * + * @example 'World English Bible' + */ + 'platform.fullName': string; + } + /** + * Names for each user setting available on the papi. + * + * Automatically includes all extensions' user settings that are added to + * {@link ProjectSettingTypes}. + * + * @example 'platform.fullName' + */ + type ProjectSettingNames = keyof ProjectSettingTypes; + /** + * The `Setting` methods required for a Project Data Provider Engine to fulfill the requirements + * of {@link MandatoryProjectDataTypes}'s `Setting` data type. + */ + type WithProjectDataProviderEngineSettingMethods< + TProjectDataTypes extends DataProviderDataTypes, + > = { + /** + * Set the value of the specified project setting on this project. + * + * @param key The string id of the project setting to change + * @param newSetting The value that is to be set to the project setting. + * @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 + */ + setSetting: ( + key: ProjectSettingName, + newSetting: ProjectSettingTypes[ProjectSettingName], + ) => Promise>; + /** + * Get the value of the specified project setting. + * + * Note: This is good for retrieving a project setting once. If you want to keep the value + * up-to-date, use `subscribeSetting` instead, which can immediately give you the value and keep + * it up-to-date. + * + * @param key The string id of the project setting to get + * @returns The value of the specified project setting. Returns default setting value if the + * project setting does not exist on the project. + * @throws If no default value is available for the setting. + */ + getSetting: ( + key: ProjectSettingName, + ) => Promise; + /** + * Deletes the specified project setting, setting it back to its default value. + * + * @param key The string id of the project setting to reset + * @returns `true` if successfully reset the project setting, `false` otherwise + */ + resetSetting: ( + key: ProjectSettingName, + ) => Promise; + }; + /** + * An object on the papi that parses raw project data from a Project Storage Interpreter and has + * methods for interacting with that project data. Created by the papi and layers over an + * {@link IProjectDataProviderEngine} provided by an extension. Returned from getting a project + * data provider with `papi.projectDataProviders.get`. + * + * Project Data Providers are a specialized version of {@link IDataProvider} that works with a + * project of a specific `projectType`. For each project available, a new instance of a PDP with + * that project's `projectType` is created by the Project Data Provider Factory with that + * project's `projectType`. + * + * Every PDP **must** fulfill the requirements of all PDPs according to + * {@link MandatoryProjectDataTypes}. + * + * Note: Project Data Providers are associated to Project Storage Interpreters based on a shared + * `projectType`. A PDP must interact with its PSI according to the + * {@link ProjectStorageProjectTypes} exposed by the PSI for that `projectType`. + */ + type IProjectDataProvider = IDataProvider< + TProjectDataTypes & MandatoryProjectDataTypes + > & + WithProjectDataProviderEngineSettingMethods & + WithProjectDataProviderEngineExtensionDataMethods & { + /** + * Subscribe to receive updates to the specified project setting. + * + * Note: By default, this `subscribeSetting` function automatically retrieves the current + * project setting value and runs the provided callback as soon as possible. That way, if you + * want to keep your data up-to-date, you do not also have to run `getSetting`. You can turn + * this functionality off in the `options` parameter. + * + * @param key The string id of the project setting for which to listen to changes + * @param callback Function to run with the updated project setting value + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber to stop listening for updates + */ + subscribeSetting: ( + key: ProjectSettingName, + callback: (value: ProjectSettingTypes[ProjectSettingName]) => void, + options: DataProviderSubscriberOptions, + ) => Promise; + }; /** This is just a simple example so we have more than one. It's not intended to be real. */ - type NotesOnlyProjectDataTypes = MandatoryProjectDataType & { + type NotesOnlyProjectDataTypes = MandatoryProjectDataTypes & { Notes: DataProviderDataType; }; /** - * `IDataProvider` types for each project data provider supported by PAPI. Extensions can add more - * project data providers with corresponding data provider IDs by adding details to their `.d.ts` - * file. Note that all project data types should extend `MandatoryProjectDataTypes` like the - * following example. + * {@link IProjectDataProvider} types for each `projectType` supported by PAPI. Extensions can add + * more Project Data Providers with corresponding `projectType`s by adding details to their + * `.d.ts` file and registering a Project Data Provider factory with the corresponding + * `projectType`. Note that all Project Data Providers' data types should extend + * {@link MandatoryProjectDataTypes} like the following example. + * + * Note: The keys of this interface are the `projectType`s for the associated Project Data + * Providers. * - * Note: Project Data Provider names must consist of two string separated by at least one period. - * We recommend one period and lower camel case in case we expand the api in the future to allow - * dot notation. + * Note: Project Data Providers are associated to Project Storage Interpreters based on a shared + * `projectType`. {@link ProjectStorageInterpreters} is sometimes indexed by {@link ProjectTypes}, + * so please make PSIs available to support the PDPs available. We recommend you specify a Project + * Storage Interpreter type on {@link ProjectStorageInterpreters} for each `projectType` for which + * you add a PDP type here in order to indicate what interface you expect to interact with in your + * PDP. * - * An extension can extend this interface to add types for the project data provider it registers - * by adding the following to its `.d.ts` file (in this example, we are adding the - * `MyExtensionProjectTypeName` data provider types): + * An extension can extend this interface to add types for the Project Data Providers its + * registered factory provides by adding the following to its `.d.ts` file (in this example, we + * are adding a Project Data Provider type for the `MyExtensionProjectTypeName` `projectType`): * * @example * * ```typescript * declare module 'papi-shared-types' { - * export type MyProjectDataType = MandatoryProjectDataType & { + * export type MyProjectDataType = MandatoryProjectDataTypes & { * MyProjectData: DataProviderDataType; * }; * @@ -2201,29 +2604,37 @@ declare module 'papi-shared-types' { * ``` */ interface ProjectDataProviders { - 'platform.notesOnly': IDataProvider; - 'platform.placeholder': IDataProvider; + 'platform.notesOnly': IProjectDataProvider; + 'platform.placeholder': IProjectDataProvider; } /** - * Names for each project data provider available on the papi. + * Names for each `projectType` available on the papi. Each of the `projectType`s should have a + * registered Project Data Provider Factory that provides Project Data Providers for the + * `projectType` along with one or more Project Storage Interpreters for the `projectType`. * - * Automatically includes all extensions' project data providers that are added to + * Automatically includes all extensions' `projectTypes` that are added to * {@link ProjectDataProviders}. * - * @example 'platform.placeholder' + * Note: {@link ProjectStorageInterpreters} is sometimes indexed by {@link ProjectTypes}, so please + * make PSIs available to support the PDPs available. + * + * @example 'platform.notesOnly' */ type ProjectTypes = keyof ProjectDataProviders; /** - * `DataProviderDataTypes` for each project data provider supported by PAPI. These are the data - * types served by each project data provider. + * `DataProviderDataTypes` for each Project Data Provider supported by PAPI. These are the data + * types served by Project Data Providers for each `projectType`. * - * Automatically includes all extensions' project data providers that are added to + * Automatically includes all extensions' `projectTypes` that are added to * {@link ProjectDataProviders}. * + * Note: The keys of this interface are the `projectType`s for the associated project data + * provider data types. + * * @example * * ```typescript - * ProjectDataTypes['MyExtensionProjectTypeName'] => { + * ProjectDataTypes['MyExtensionProjectTypeName'] => MandatoryProjectDataTypes & { * MyProjectData: DataProviderDataType; * } * ``` @@ -2231,6 +2642,185 @@ declare module 'papi-shared-types' { type ProjectDataTypes = { [ProjectType in ProjectTypes]: ExtractDataProviderDataTypes; }; + /** + * Indicates to a Project Storage Interpreter what project setting is being referenced on what + * project. Generally, a Project Data Provider passes calls to `Setting` data type methods to its + * PSI and adds the `projectId`. + */ + type ProjectStorageSettingDataScope = { + /** Key of the Project Setting to select */ + key: ProjectSettingName; + /** ID for the project whose extension data to get */ + projectId: string; + }; + /** + * An object on the papi that manages raw project data and has methods for a Project Data Provider + * to interact with that raw project data. Created by the papi and layers over an + * {@link IProjectStorageInterpreterEngine} provided by an extension. + * + * Project Storage Interpreters are a specialized version of {@link IDataProvider} that works with + * projects of a specific `storageType` and one or more `projectType`s. For each project + * available, a PDP with that project's `projectType` will interact with the PSI with that + * project's `storageType` and `projectType`. + * + * Every PSI **must** fulfill the requirements of all PSIs according to + * {@link MandatoryProjectStorageDataTypes}. + * + * Note: Project Data Providers are associated to Project Storage Interpreters based on a shared + * `projectType`. A PSI must implement the {@link ProjectStorageProjectTypes} specified for each + * `projectType` it supports. + * + * Using this interface without specifying data types will default to using + * {@link DefaultProjectStorageDataTypes} as a sensible default method of communication between a + * PDP and a PSI for a specific `projectType`. + */ + type IProjectStorageInterpreter< + TProjectStorageDataTypes extends DataProviderDataTypes = DefaultProjectStorageDataTypes, + > = IDataProvider & + IDataProvider & { + /** + * Set the value of the specified project setting on this project. + * + * @param settingDataScope The string id of the project setting to change and the project on + * which to change it + * @param newSetting The value that is to be set to the project setting. + * @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 + */ + setSetting: ( + settingDataScope: ProjectStorageSettingDataScope, + newSetting: ProjectSettingTypes[ProjectSettingName], + ) => Promise>; + /** + * Get the value of the specified project setting. + * + * Note: This is good for retrieving a project setting once. If you want to keep the value + * up-to-date, use `subscribeSetting` instead, which can immediately give you the value and + * keep it up-to-date. + * + * @param settingDataScope The string id of the project setting to get and the project from + * which to get it + * @returns The value of the specified project setting. Returns default setting value if the + * project setting does not exist on the project. + * @throws If no default value is available for the setting. + */ + getSetting: ( + settingDataScope: ProjectStorageSettingDataScope, + ) => Promise; + /** + * Subscribe to receive updates to the specified project setting. + * + * Note: By default, this `subscribeSetting` function automatically retrieves the current + * project setting value and runs the provided callback as soon as possible. That way, if you + * want to keep your data up-to-date, you do not also have to run `getSetting`. You can turn + * this functionality off in the `options` parameter. + * + * @param settingDataScope The string id of the project setting for which to listen to changes + * and the project on which to listen + * @param callback Function to run with the updated project setting value + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber to stop listening for updates + */ + subscribeSetting: ( + settingDataScope: ProjectStorageSettingDataScope, + callback: (value: ProjectSettingTypes[ProjectSettingName]) => void, + options: DataProviderSubscriberOptions, + ) => Promise; + /** + * Deletes the specified project setting, setting it back to its default value. + * + * @param settingDataScope The string id of the project setting to reset and the project on + * which to reset it + * @returns `true` if successfully reset the project setting, `false` otherwise + */ + resetSetting: ( + settingDataScope: ProjectStorageSettingDataScope, + ) => Promise; + }; + /** + * {@link IProjectStorageInterpreter} types for each `projectType` supported by PAPI. Extensions + * can add more Project Storage Interpreters that support corresponding `projectType`s by adding + * details to their `.d.ts` file and registering a Project Storage Interpreter that supports the + * corresponding `projectType`. Note that all Project Storage Interpreters' data types should + * extend {@link MandatoryProjectStorageDataTypes} like the following example. + * + * Note: The keys of this interface are the `projectType`s supported by available Project Storage + * Interpreters. + * + * WARNING: Each Project Storage Interpreter **must** fulfill certain requirements for its + * `getSetting`, `setSetting`, and `resetSetting` methods. See + * {@link MandatoryProjectStorageDataTypes} for more information. + * + * An extension can extend this interface to add types for the `projectType`s its Project Storage + * Interpreters support by adding the following to its `.d.ts` file (in this example, we are + * adding a Project Storage Interpreter type for the `MyExtensionProjectTypeName` `projectType`): + * + * @example + * + * ```typescript + * declare module 'papi-shared-types' { + * export type MyProjectStorageDataType = MandatoryProjectStorageDataTypes & { + * ProjectData: DataProviderDataType< + * { projectId: string; section: number }, + * string | undefined, + * string + * >; + * }; + * + * export interface ProjectStorageInterpreters { + * MyExtensionProjectTypeName: IProjectStorageInterpreter; + * } + * } + * ``` + */ + interface ProjectStorageInterpreters { + 'platform.notesOnly': IProjectStorageInterpreter; + 'platform.placeholder': IProjectStorageInterpreter; + } + /** + * Names for each `projectType` supported by available Project Storage Interpreters on the papi. + * Each of the `projectType`s should have a registered Project Data Provider Factory that provides + * Project Data Providers for the `projectType` along with one or more Project Storage + * Interpreters for the `projectType`. + * + * Automatically includes all extensions' `projectTypes` that are added to + * {@link ProjectStorageInterpreters}. + * + * Note: {@link ProjectStorageInterpreters} is sometimes indexed by {@link ProjectTypes}, so please + * make PSIs available to support the PDPs available. + * + * @example 'platform.notesOnly' + */ + type ProjectStorageProjectTypes = keyof ProjectStorageInterpreters; + /** + * `DataProviderDataTypes` for each Project Storage Interpreter supported by PAPI. These are the + * data types served by Project Storage Interpreters to Project Data Providers for each + * `projectType`. + * + * Automatically includes all extensions' `projectTypes` that are added to + * {@link ProjectStorageInterpreters}. + * + * Note: The keys of this interface are the `projectType`s supported by available Project Storage + * Interpreters. + * + * @example + * + * ```typescript + * ProjectStorageDataTypes['MyExtensionProjectTypeName'] => MandatoryProjectStorageDataTypes & { + * ProjectData: DataProviderDataType< + * { projectId: string; section: number }, + * string | undefined, + * string + * >; + * } + * ``` + */ + type ProjectStorageDataTypes = { + [ProjectType in ProjectStorageProjectTypes]: ExtractDataProviderDataTypes< + ProjectStorageInterpreters[ProjectType] + >; + }; type StuffDataTypes = { Stuff: DataProviderDataType; }; @@ -2244,12 +2834,12 @@ declare module 'papi-shared-types' { >; }; /** - * `IDataProvider` types for each data provider supported by PAPI. Extensions can add more data - * providers with corresponding data provider IDs by adding details to their `.d.ts` file and + * {@link IDataProvider} types for each data provider supported by PAPI. Extensions can add more + * data providers with corresponding data provider IDs by adding details to their `.d.ts` file and * registering a data provider engine in their `activate` function with * `papi.dataProviders.registerEngine`. * - * Note: Data Provider names must consist of two string separated by at least one period. We + * Note: Data Provider names must consist of two strings separated by at least one period. We * recommend one period and lower camel case in case we expand the api in the future to allow dot * notation. * @@ -2740,7 +3330,7 @@ declare module 'shared/services/data-provider.service' { /** Handles registering data providers and serving data around the papi. Exposed on the papi. */ import { DataProviderDataTypes } from 'shared/models/data-provider.model'; import IDataProviderEngine, { - DataProviderEngineNotifyUpdate, + DataProviderEngine, } from 'shared/models/data-provider-engine.model'; import { DataProviderNames, @@ -2751,22 +3341,13 @@ declare module 'shared/services/data-provider.service' { import IDataProvider, { IDisposableDataProvider } from 'shared/models/data-provider.interface'; /** * - * Abstract class that provides a placeholder `notifyUpdate` for data provider engine classes. If a - * data provider engine class extends this class, it doesn't have to specify its own `notifyUpdate` - * function in order to use `notifyUpdate`. - * - * @see IDataProviderEngine for more information on extending this class. - */ - export abstract class DataProviderEngine { - notifyUpdate: DataProviderEngineNotifyUpdate; - } - /** * Indicate if we are aware of an existing data provider with the given name. If a data provider * with the given name is somewhere else on the network, this function won't tell you about it * unless something else in the existing process is subscribed to it. */ function hasKnown(providerName: string): boolean; /** + * * Decorator function that marks a data provider engine `set___` or `get___` method to be ignored. * papi will not layer over these methods or consider them to be data type methods * @@ -2811,8 +3392,58 @@ declare module 'shared/services/data-provider.service' { * Note: this is the signature that provides the actual decorator functionality. However, since * users will not be using this signature, the example usage is provided in the signature above. */ - function ignore(target: T, member: keyof T): void; + function ignore(target: object, member: string): void; + /** + * + * Decorator function that marks a data provider engine `set` method not to automatically + * emit an update and notify subscribers of a change to the data. papi will still consider the + * `set` method to be a data type method, but it will not layer over it to emit updates. + * + * @example Use this as a decorator on a class's method: + * + * ```typescript + * class MyDataProviderEngine { + * @papi.dataProviders.decorators.doNotNotify + * async setVerse() {} + * } + * ``` + * + * WARNING: Do not copy and paste this example. The `@` symbol does not render correctly in JSDoc + * code blocks, so a different unicode character was used. Please use a normal `@` when using a + * decorator. + * + * OR + * + * @example Call this function signature on an object's method: + * + * ```typescript + * const myDataProviderEngine = { + * async setVerse() {}, + * }; + * papi.dataProviders.decorators.ignore(dataProviderEngine.setVerse); + * ``` + * + * @param method The method not to layer over to send an automatic update + */ + function doNotNotify( + method: Function & { + doNotNotify?: boolean; + }, + ): void; + /** + * Decorator function that marks a data provider engine `set` method not to automatically + * emit an update and notify subscribers of a change to the data. papi will still consider the + * `set` method to be a data type method, but it will not layer over it to emit updates. + * + * @param target The class that has the method not to layer over to send an automatic update + * @param member The name of the method not to layer over to send an automatic update + * + * Note: this is the signature that provides the actual decorator functionality. However, since + * users will not be using this signature, the example usage is provided in the signature above. + */ + function doNotNotify(target: object, member: string): void; /** + * * A collection of decorators to be used with the data provider service * * @example To use the `ignore` a decorator on a class's method: @@ -2829,9 +3460,74 @@ declare module 'shared/services/data-provider.service' { * decorator. */ const decorators: { + /** + * + * Decorator function that marks a data provider engine `set___` or `get___` method to be ignored. + * papi will not layer over these methods or consider them to be data type methods + * + * @example Use this as a decorator on a class's method: + * + * ```typescript + * class MyDataProviderEngine { + * @papi.dataProviders.decorators.ignore + * async getInternal() {} + * } + * ``` + * + * WARNING: Do not copy and paste this example. The `@` symbol does not render correctly in JSDoc + * code blocks, so a different unicode character was used. Please use a normal `@` when using a + * decorator. + * + * OR + * + * @example Call this function signature on an object's method: + * + * ```typescript + * const myDataProviderEngine = { + * async getInternal() {}, + * }; + * papi.dataProviders.decorators.ignore(dataProviderEngine.getInternal); + * ``` + * + * @param method The method to ignore + */ ignore: typeof ignore; + /** + * + * Decorator function that marks a data provider engine `set` method not to automatically + * emit an update and notify subscribers of a change to the data. papi will still consider the + * `set` method to be a data type method, but it will not layer over it to emit updates. + * + * @example Use this as a decorator on a class's method: + * + * ```typescript + * class MyDataProviderEngine { + * @papi.dataProviders.decorators.doNotNotify + * async setVerse() {} + * } + * ``` + * + * WARNING: Do not copy and paste this example. The `@` symbol does not render correctly in JSDoc + * code blocks, so a different unicode character was used. Please use a normal `@` when using a + * decorator. + * + * OR + * + * @example Call this function signature on an object's method: + * + * ```typescript + * const myDataProviderEngine = { + * async setVerse() {}, + * }; + * papi.dataProviders.decorators.ignore(dataProviderEngine.setVerse); + * ``` + * + * @param method The method not to layer over to send an automatic update + */ + doNotNotify: typeof doNotNotify; }; /** + * * Creates a data provider to be shared on the network layering over the provided data provider * engine. * @@ -2893,6 +3589,7 @@ declare module 'shared/services/data-provider.service' { | undefined, ): Promise>>; /** + * * Get a data provider that has previously been set up * * @param providerName Name of the desired data provider @@ -2917,10 +3614,67 @@ declare module 'shared/services/data-provider.service' { providerName: string, ): Promise; export interface DataProviderService { + /** + * + * Indicate if we are aware of an existing data provider with the given name. If a data provider + * with the given name is somewhere else on the network, this function won't tell you about it + * unless something else in the existing process is subscribed to it. + */ hasKnown: typeof hasKnown; + /** + * + * Creates a data provider to be shared on the network layering over the provided data provider + * engine. + * + * @param providerName Name this data provider should be called on the network + * @param dataProviderEngine The object to layer over with a new data provider object + * @param dataProviderType String to send in a network event to clarify what type of data provider + * is represented by this engine. For generic data providers, the default value of `dataProvider` + * can be used. For data provider types that have multiple instances (e.g., project data + * providers), a unique type name should be used to distinguish from generic data providers. + * @param dataProviderAttributes Optional object that will be sent in a network event to provide + * additional metadata about the data provider represented by this engine. + * + * WARNING: registering a dataProviderEngine mutates the provided object. Its `notifyUpdate` and + * `set` methods are layered over to facilitate data provider subscriptions. + * @returns The data provider including control over disposing of it. Note that this data provider + * is a new object distinct from the data provider engine passed in. + */ registerEngine: typeof registerEngine; + /** + * + * Get a data provider that has previously been set up + * + * @param providerName Name of the desired data provider + * @returns The data provider with the given name if one exists, undefined otherwise + */ get: typeof get; + /** + * + * A collection of decorators to be used with the data provider service + * + * @example To use the `ignore` a decorator on a class's method: + * + * ```typescript + * class MyDataProviderEngine { + * @papi.dataProviders.decorators.ignore + * async getInternal() {} + * } + * ``` + * + * WARNING: Do not copy and paste this example. The `@` symbol does not render correctly in JSDoc + * code blocks, so a different unicode character was used. Please use a normal `@` when using a + * decorator. + */ decorators: typeof decorators; + /** + * + * Abstract class that provides a placeholder `notifyUpdate` for data provider engine classes. If a + * data provider engine class extends this class, it doesn't have to specify its own `notifyUpdate` + * function in order to use `notifyUpdate`. + * + * @see {@link IDataProviderEngine} for more information on extending this class. + */ DataProviderEngine: typeof DataProviderEngine; } /** @@ -2931,11 +3685,32 @@ declare module 'shared/services/data-provider.service' { export default dataProviderService; } declare module 'shared/models/project-data-provider-engine.model' { - import { ProjectTypes, ProjectDataTypes } from 'papi-shared-types'; - import type IDataProviderEngine from 'shared/models/data-provider-engine.model'; - /** All possible types for ProjectDataProviderEngines: IDataProviderEngine */ + import { + ProjectTypes, + ProjectDataTypes, + ProjectStorageInterpreters, + ProjectSettingNames, + ProjectSettingTypes, + ProjectStorageSettingDataScope, + WithProjectDataProviderEngineSettingMethods, + } from 'papi-shared-types'; + import IDataProviderEngine, { + DataProviderEngine, + } from 'shared/models/data-provider-engine.model'; + import { + DataProviderDataType, + DataProviderDataTypes, + DataProviderUpdateInstructions, + } from 'shared/models/data-provider.model'; + import { + ExtensionDataScope, + MandatoryProjectDataTypes, + WithProjectDataProviderEngineExtensionDataMethods, + } from 'shared/models/project-data-provider.model'; + import { ProjectStorageExtensionDataScope } from 'shared/models/project-storage-interpreter.model'; + /** All possible types for ProjectDataProviderEngines: IProjectDataProviderEngine */ export type ProjectDataProviderEngineTypes = { - [ProjectType in ProjectTypes]: IDataProviderEngine; + [ProjectType in ProjectTypes]: IProjectDataProviderEngine; }; export interface ProjectDataProviderEngineFactory { createProjectDataProviderEngine( @@ -2943,6 +3718,143 @@ declare module 'shared/models/project-data-provider-engine.model' { projectStorageInterpreterId: string, ): ProjectDataProviderEngineTypes[ProjectType]; } + /** + * The object to return from {@link ProjectDataProviderEngineFactory.createProjectDataProviderEngine} + * that the PAPI registers to create a Project Data Provider for a specific project. The + * ProjectDataProviderService creates an {@link IProjectDataProvider} on the papi that layers over + * this engine, providing special functionality. + * + * @type TProjectDataTypes - The data types that this Project Data Provider Engine serves. For each + * data type defined, the engine must have corresponding `get` and `set + * function` functions. These data types correspond to the `projectType` of the project. + * @see {@link DataProviderDataTypes} for information on how to make powerful types that work well with + * Intellisense. + * + * Note: papi creates a `notifyUpdate` function on the Project Data Provider Engine if one is not + * provided, so it is not necessary to provide one in order to call `this.notifyUpdate`. However, + * TypeScript does not understand that papi will create one as you are writing your Project Data + * Provider Engine, so you can avoid type errors with one of the following options: + * + * 1. If you are using a class to create a Project Data Provider Engine, you can extend the + * {@link ProjectDataProviderEngine} class, and it will provide `notifyUpdate` as well as other helpful + * default method implementations to meet the requirements of {@link MandatoryProjectDataTypes} + * automatically by passing these calls through to the Project Storage Interpreter for you: + * ```typescript + * class MyPDPE extends DataProviderEngine implements IDataProviderEngine { + * ... + * } + * ``` + * + * 2. If you are using an object or class to create a Project Data Provider Engine, you can add a + * `notifyUpdate` function (and, with an object, add the {@link WithNotifyUpdate} type) to + * your Project Data Provider Engine like so: + * ```typescript + * const myPDPE: IProjectDataProviderEngine & WithNotifyUpdate = { + * notifyUpdate(updateInstructions) {}, + * ... + * } + * ``` + * OR + * ```typescript + * class MyPDPE implements IProjectDataProviderEngine { + * notifyUpdate(updateInstructions?: DataProviderEngineNotifyUpdate) {} + * ... + * } + * ``` + */ + export type IProjectDataProviderEngine = + IDataProviderEngine & + WithProjectDataProviderEngineSettingMethods & + WithProjectDataProviderEngineExtensionDataMethods; + /** + * + * Abstract class that provides default implementations of a number of {@link IProjectDataProvider} + * functions including all the `Setting` and `ExtensionData`-related methods. Extensions can create + * their own Project Data Provider Engine classes and implement this class to meet the requirements + * of {@link MandatoryProjectDataTypes} automatically by passing these calls through to the Project + * Storage Interpreter. This class also subscribes to `Setting` and `ExtensionData` updates from the + * PSI to make sure it keeps its data up-to-date. + * + * This class also provides a placeholder `notifyUpdate` for Project Data Provider Engine classes. + * If a Project Data Provider Engine class extends this class, it doesn't have to specify its own + * `notifyUpdate` function in order to use `notifyUpdate`. + * + * @see {@link IProjectDataProviderEngine} for more information on extending this class. + */ + export abstract class ProjectDataProviderEngine + extends DataProviderEngine< + ProjectDataTypes[ProjectType] & { + Setting: DataProviderDataType< + ProjectSettingNames, + ProjectSettingTypes[ProjectSettingNames], + ProjectSettingTypes[ProjectSettingNames] + >; + } + > + implements + WithProjectDataProviderEngineSettingMethods, + WithProjectDataProviderEngineExtensionDataMethods + { + protected readonly projectId: string; + protected readonly projectStorageInterpreterId: string; + protected readonly projectStorageInterpreter: ProjectStorageInterpreters[ProjectType]; + private psiSettingUnsubscriberPromise; + private psiExtensionDataUnsubscriberPromise; + /** + * Create a `ProjectDataProviderEngine` instance + * + * @param projectId The project id this Project Data Provider represents for which it serves data + * @param projectStorageInterpreter The {@link IProjectStorageInterpreter} this Project Data + * Provider uses to access raw project data + */ + protected constructor(projectId: string, projectStorageInterpreterId: string); + setSetting( + key: ProjectSettingName, + newSetting: ProjectSettingTypes[ProjectSettingName], + ): Promise>; + setExtensionData( + dataScope: ExtensionDataScope, + data: string, + ): Promise>; + /** + * Get the {@link ProjectStorageSettingDataScope} for this project for the specified project + * setting. This can be used when passing setting-related calls to the Project Storage + * Interpreter. + * + * @param key The string id of the project setting to get a setting data scope for + * @returns `ProjectStorageSettingDataScope` for this project for the specified project setting + */ + protected getProjectStorageSettingDataScope( + key: ProjectSettingName, + ): ProjectStorageSettingDataScope; + /** + * Get the {@link ProjectStorageExtensionDataScope} for this project for the specified + * {@link ExtensionDataScope}. This can be used when passing `ExtensionData`-related calls to the + * Project Storage Interpreter. + * + * @param dataScope Information about what data is being referenced by the calling extension given + * to this Project Data Provider + * @returns Information about what data is being referenced by the calling extension that this + * Project Data Provider should give to its Project Storage Interpreter + */ + protected getProjectStorageExtensionDataScope( + dataScope: ExtensionDataScope, + ): ProjectStorageExtensionDataScope; + getSetting( + key: ProjectSettingName, + ): Promise; + resetSetting( + key: ProjectSettingName, + ): Promise; + getExtensionData(dataScope: ExtensionDataScope): Promise; + /** + * Disposes of this Project Data Provider Engine. Unsubscribes from listening to the Project + * Storage Interpreter + * + * @returns `true` if successfully unsubscribed + */ + dispose(): Promise; + } } declare module 'shared/models/project-metadata.model' { import { ProjectTypes } from 'papi-shared-types'; @@ -3738,6 +4650,69 @@ declare module 'renderer/hooks/papi-hooks/use-dialog-callback.hook' { ): (optionOverrides?: Partial) => Promise; export default useDialogCallback; } +declare module 'shared/services/project-settings.service-model' { + import { ProjectSettingNames, ProjectSettingTypes, ProjectTypes } from 'papi-shared-types'; + /** + * + * Provides utility functions that project storage interpreters should call when handling project + * settings + */ + export interface IProjectSettingsService { + /** + * Calls registered project settings validators to determine whether or not a project setting + * change is valid. + * + * Every Project Storage Interpreter **must** run this function when it receives a request to set + * a project setting before changing the value of the setting. + * + * @param newValue The new value requested to set the project setting value to + * @param currentValue The current project setting value + * @param key The project setting key being set + * @param allChanges All project settings changes being set in one batch + * @param projectType The `projectType` for the project whose setting is being changed + * @returns `true` if change is valid, `false` otherwise + */ + isValid( + newValue: ProjectSettingTypes[ProjectSettingName], + currentValue: ProjectSettingTypes[ProjectSettingName], + key: ProjectSettingName, + allChanges: SimultaneousProjectSettingsChanges, + projectType: ProjectTypes, + ): Promise; + /** + * Gets default value for a project setting + * + * Every Project Storage Interpreter **must** run this function when it receives a request to get + * a project setting if the project does not have a value for the project setting requested. It + * should return the response from this function directly, either the returned default value or + * throw. + * + * @param key The project setting key for which to get the default value + * @param projectType The `projectType` to get default setting value for + * @returns The default value for the setting if a default value is registered + * @throws If a default value is not registered for the setting + */ + getDefault( + key: ProjectSettingName, + projectType: ProjectTypes, + ): Promise; + } + /** + * All project settings changes being set in one batch + * + * Project settings may be circularly dependent on one another, so multiple project settings may + * need to be changed at once in some cases + */ + export type SimultaneousProjectSettingsChanges = { + [ProjectSettingName in ProjectSettingNames]?: { + /** The new value requested to set the project setting value to */ + newValue: ProjectSettingTypes[ProjectSettingName]; + /** The current project setting value */ + currentValue: ProjectSettingTypes[ProjectSettingName]; + }; + }; + export const projectSettingsServiceNetworkObjectName = 'ProjectSettingsService'; +} declare module '@papi/core' { /** Exporting empty object so people don't have to put 'type' in their import statements */ const core: {}; @@ -3757,9 +4732,10 @@ declare module '@papi/core' { export type { DialogOptions } from 'shared/models/dialog-options.model'; export type { ExtensionDataScope, - MandatoryProjectDataType, + MandatoryProjectDataTypes, } from 'shared/models/project-data-provider.model'; export type { ProjectMetadata } from 'shared/models/project-metadata.model'; + export type { MandatoryProjectStorageDataTypes } from 'shared/models/project-storage-interpreter.model'; export type { GetWebViewOptions, SavedWebViewDefinition, @@ -3769,6 +4745,7 @@ declare module '@papi/core' { WebViewProps, } from 'shared/models/web-view.model'; export type { IWebViewProvider } from 'shared/models/web-view-provider.model'; + export type { SimultaneousProjectSettingsChanges } from 'shared/services/project-settings.service-model'; } declare module 'shared/services/menu-data.service-model' { import { @@ -3924,6 +4901,12 @@ declare module 'shared/services/settings.service-model' { * 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. + * + * The closest possible representation of the unnamed (````) data type follows: + * + * ```typescript + * '': DataProviderDataType; + * ``` */ export type SettingDataTypes = {}; export type AllSettingsData = { @@ -3988,6 +4971,11 @@ declare module 'shared/services/settings.service' { const settingsService: ISettingsService; export default settingsService; } +declare module 'shared/services/project-settings.service' { + import { IProjectSettingsService } from 'shared/services/project-settings.service-model'; + const projectSettingsService: IProjectSettingsService; + export default projectSettingsService; +} declare module '@papi/backend' { /** * Unified module for accessing API features in the extension host. @@ -3999,16 +4987,16 @@ declare module '@papi/backend' { import { WebViewServiceType } from 'shared/services/web-view.service-model'; import { PapiWebViewProviderService } from 'shared/services/web-view-provider.service'; import { InternetService } from 'shared/services/internet.service'; - import { - DataProviderService, - DataProviderEngine as PapiDataProviderEngine, - } from 'shared/services/data-provider.service'; + import { DataProviderService } from 'shared/services/data-provider.service'; + import { DataProviderEngine as PapiDataProviderEngine } from 'shared/models/data-provider-engine.model'; import { PapiBackendProjectDataProviderService } from 'shared/services/project-data-provider.service'; import { ExtensionStorageService } from 'extension-host/services/extension-storage.service'; 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'; + import { IProjectSettingsService } from 'shared/services/project-settings.service-model'; + import { ProjectDataProviderEngine as PapiProjectDataProviderEngine } from 'shared/models/project-data-provider-engine.model'; const papi: { /** * @@ -4016,9 +5004,25 @@ declare module '@papi/backend' { * data provider engine class extends this class, it doesn't have to specify its own `notifyUpdate` * function in order to use `notifyUpdate`. * - * @see IDataProviderEngine for more information on extending this class. + * @see {@link IDataProviderEngine} for more information on extending this class. */ DataProviderEngine: typeof PapiDataProviderEngine; + /** + * + * Abstract class that provides default implementations of a number of {@link IProjectDataProvider} + * functions including all the `Setting` and `ExtensionData`-related methods. Extensions can create + * their own Project Data Provider Engine classes and implement this class to meet the requirements + * of {@link MandatoryProjectDataTypes} automatically by passing these calls through to the Project + * Storage Interpreter. This class also subscribes to `Setting` and `ExtensionData` updates from the + * PSI to make sure it keeps its data up-to-date. + * + * This class also provides a placeholder `notifyUpdate` for Project Data Provider Engine classes. + * If a Project Data Provider Engine class extends this class, it doesn't have to specify its own + * `notifyUpdate` function in order to use `notifyUpdate`. + * + * @see {@link IProjectDataProviderEngine} for more information on extending this class. + */ + ProjectDataProviderEngine: typeof PapiProjectDataProviderEngine; /** This is just an alias for internet.fetch */ fetch: typeof globalThis.fetch; /** @@ -4078,6 +5082,12 @@ declare module '@papi/backend' { * Provides metadata for projects known by the platform */ projectLookup: ProjectLookupServiceType; + /** + * + * Provides utility functions that project storage interpreters should call when handling project + * settings + */ + projectSettings: IProjectSettingsService; /** * * This service provides extensions in the extension host the ability to read/write data based on @@ -4100,9 +5110,25 @@ declare module '@papi/backend' { * data provider engine class extends this class, it doesn't have to specify its own `notifyUpdate` * function in order to use `notifyUpdate`. * - * @see IDataProviderEngine for more information on extending this class. + * @see {@link IDataProviderEngine} for more information on extending this class. */ export const DataProviderEngine: typeof PapiDataProviderEngine; + /** + * + * Abstract class that provides default implementations of a number of {@link IProjectDataProvider} + * functions including all the `Setting` and `ExtensionData`-related methods. Extensions can create + * their own Project Data Provider Engine classes and implement this class to meet the requirements + * of {@link MandatoryProjectDataTypes} automatically by passing these calls through to the Project + * Storage Interpreter. This class also subscribes to `Setting` and `ExtensionData` updates from the + * PSI to make sure it keeps its data up-to-date. + * + * This class also provides a placeholder `notifyUpdate` for Project Data Provider Engine classes. + * If a Project Data Provider Engine class extends this class, it doesn't have to specify its own + * `notifyUpdate` function in order to use `notifyUpdate`. + * + * @see {@link IProjectDataProviderEngine} for more information on extending this class. + */ + export const ProjectDataProviderEngine: typeof PapiProjectDataProviderEngine; /** This is just an alias for internet.fetch */ export const fetch: typeof globalThis.fetch; /** @@ -4162,6 +5188,12 @@ declare module '@papi/backend' { * Provides metadata for projects known by the platform */ export const projectLookup: ProjectLookupServiceType; + /** + * + * Provides utility functions that project storage interpreters should call when handling project + * settings + */ + export const projectSettings: IProjectSettingsService; /** * * This service provides extensions in the extension host the ability to read/write data based on @@ -4441,9 +5473,9 @@ declare module 'renderer/hooks/papi-hooks/use-data.hook' { * _@returns_ `[data, setData, isLoading]` * * - `data`: the current value for the data from the data provider with the specified data type and - * selector, either the defaultValue or the resolved data + * selector, either the `defaultValue` or the resolved data * - `setData`: asynchronous function to request that the data provider update the data at this data - * type and selector. Returns true if successful. Note that this function does not update the + * type and selector. Returns `true` if successful. Note that this function does not update the * data. The data provider sends out an update to this subscription if it successfully updates * data. * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the data @@ -4504,15 +5536,15 @@ declare module 'renderer/hooks/papi-hooks/use-project-data-provider.hook' { * Gets a project data provider with specified provider name * * @param projectType Indicates what you expect the `projectType` to be for the project with the - * specified id. The TypeScript type for the returned project data provider will have the project - * data provider type associated with this project type. If this argument does not match the + * specified id. The TypeScript type for the returned Project Data Provider will have the Project + * Data Provider type associated with this `projectType`. If this argument does not match the * project's actual `projectType` (according to its metadata), a warning will be logged * @param projectDataProviderSource String name of the id of the project to get OR * projectDataProvider (result of useProjectDataProvider, if you want this hook to just return the * data provider again) - * @returns `undefined` if the project data provider has not been retrieved, the requested project - * data provider if it has been retrieved and is not disposed, and undefined again if the project - * data provider is disposed + * @returns `undefined` if the Project Data Provider has not been retrieved, the requested Project + * Data Provider if it has been retrieved and is not disposed, and undefined again if the Project + * Data Provider is disposed */ const useProjectDataProvider: ( projectType: ProjectType, @@ -4527,7 +5559,7 @@ declare module 'renderer/hooks/papi-hooks/use-project-data.hook' { } from 'shared/models/data-provider.model'; import { ProjectDataProviders, ProjectDataTypes, ProjectTypes } from 'papi-shared-types'; /** - * React hook to use data from a project data provider + * React hook to use data from a Project Data Provider * * @example `useProjectData('ParatextStandard', 'project id').VerseUSFM(...);` */ @@ -4577,11 +5609,11 @@ declare module 'renderer/hooks/papi-hooks/use-project-data.hook' { * ] * ``` * - * React hook to use data from a project data provider. Subscribes to run a callback on a project - * data provider's data with specified selector on the specified data type that the project data - * provider serves according to its `projectType`. + * React hook to use data from a Project Data Provider. Subscribes to run a callback on a Project + * Data Provider's data with specified selector on the specified data type that the Project Data + * Provider serves according to its `projectType`. * - * Usage: Specify the project type, the project id, and the data type on the project data provider + * Usage: Specify the project type, the project id, and the data type on the Project Data Provider * with `useProjectData('', '').` and use like any other React * hook. * @@ -4599,13 +5631,13 @@ declare module 'renderer/hooks/papi-hooks/use-project-data.hook' { * ``` * * _@param_ `projectType` Indicates what you expect the `projectType` to be for the project with the - * specified id. The TypeScript type for the returned project data provider will have the project - * data provider type associated with this project type. If this argument does not match the + * specified id. The TypeScript type for the returned Project Data Provider will have the Project + * Data Provider type associated with this project type. If this argument does not match the * project's actual `projectType` (according to its metadata), a warning will be logged * * _@param_ `projectDataProviderSource` String name of the id of the project to get OR * projectDataProvider (result of useProjectDataProvider if you want to consolidate and only get the - * project data provider once) + * Project Data Provider once) * * _@param_ `selector` tells the provider what data this listener is listening for * @@ -4617,15 +5649,82 @@ declare module 'renderer/hooks/papi-hooks/use-project-data.hook' { * _@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. This means that `subscriberOptions` will be passed to the project - * data provider's `subscribe` method as soon as possible and will not be updated again + * to re-run with its new value. This means that `subscriberOptions` will be passed to the Project + * Data Provider's `subscribe` method as soon as possible and will not be updated again * until `projectDataProviderSource` or `selector` changes. * * _@returns_ `[data, setData, isLoading]` + * + * - `data`: the current value for the data from the Project Data Provider with the specified data + * type and selector, either the `defaultValue` or the resolved data + * - `setData`: asynchronous function to request that the Project Data Provider update the data at + * this data type and selector. Returns `true` if successful. Note that this function does not + * update the data. The Project Data Provider sends out an update to this subscription if it + * successfully updates data. + * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the data + * provider */ const useProjectData: UseProjectDataHook; export default useProjectData; } +declare module 'renderer/hooks/papi-hooks/use-project-setting.hook' { + import { ProjectDataProviders, ProjectSettingTypes } from 'papi-shared-types'; + import { DataProviderSubscriberOptions } from 'shared/models/data-provider.model'; + /** + * Gets, sets and resets a project setting on the papi for a specified project. Also notifies + * subscribers when the project setting changes and gets updated when the project setting is changed + * by others. + * + * @param projectType Indicates what you expect the `projectType` to be for the project with the + * specified id. The TypeScript type for the returned Project Data Provider will have the Project + * Data Provider type associated with this `projectType`. If this argument does not match the + * project's actual `projectType` (according to its metadata), a warning will be logged + * @param projectDataProviderSource `projectDataProviderSource` String name of the id of the project + * to get OR projectDataProvider (result of `useProjectDataProvider` if you want to consolidate + * and only get the Project Data Provider once) + * @param key The string id of the project setting to interact with + * + * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be + * updated every render + * @param defaultValue The initial value to return while first awaiting the project 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. This means that `subscriberOptions` will be passed to the data + * provider's `subscribe` method as soon as possible and will not be updated again + * until `dataProviderSource` or `selector` changes. + * @returns `[setting, setSetting, resetSetting]` + * + * - `setting`: the current value for the project setting from the Project Data Provider with the + * specified key, either the `defaultValue` or the resolved setting value + * - `setSetting`: asynchronous function to request that the Project Data Provider update the project + * setting with the specified key. Returns `true` if successful. Note that this function does + * not update the data. The Project Data Provider sends out an update to this subscription if + * it successfully updates data. + * - `resetSetting`: asynchronous function to request that the Project Data Provider reset the project + * setting + * - `isLoading`: whether the setting value is awaiting retrieval from the Project Data Provider + * + * @throws When subscription callback function is called with an update that has an unexpected + * message type + */ + const useProjectSetting: < + ProjectType extends keyof ProjectDataProviders, + ProjectSettingName extends keyof ProjectSettingTypes, + >( + projectType: ProjectType, + projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined, + key: ProjectSettingName, + defaultValue: ProjectSettingTypes[ProjectSettingName], + subscriberOptions?: DataProviderSubscriberOptions, + ) => [ + setting: ProjectSettingTypes[ProjectSettingName], + setSetting: ((newSetting: ProjectSettingTypes[ProjectSettingName]) => void) | undefined, + resetSetting: (() => void) | undefined, + isLoading: boolean, + ]; + export default useProjectSetting; +} declare module 'renderer/hooks/papi-hooks/use-data-provider-multi.hook' { import { DataProviderNames, DataProviders } from 'papi-shared-types'; /** @@ -4666,6 +5765,7 @@ declare module 'renderer/hooks/papi-hooks/index' { export { default as useSetting } from 'renderer/hooks/papi-hooks/use-setting.hook'; export { default as useProjectData } from 'renderer/hooks/papi-hooks/use-project-data.hook'; export { default as useProjectDataProvider } from 'renderer/hooks/papi-hooks/use-project-data-provider.hook'; + export { default as useProjectSetting } from 'renderer/hooks/papi-hooks/use-project-setting.hook'; export { default as useDialogCallback } from 'renderer/hooks/papi-hooks/use-dialog-callback.hook'; export { default as useDataProviderMulti } from 'renderer/hooks/papi-hooks/use-data-provider-multi.hook'; } diff --git a/src/declarations/papi-shared-types.ts b/src/declarations/papi-shared-types.ts index f1938bc1f7..6a27b9d236 100644 --- a/src/declarations/papi-shared-types.ts +++ b/src/declarations/papi-shared-types.ts @@ -1,17 +1,31 @@ declare module 'papi-shared-types' { - import type { ScriptureReference } from 'platform-bible-utils'; - import type { DataProviderDataType } from '@shared/models/data-provider.model'; - import type { MandatoryProjectDataType } from '@shared/models/project-data-provider.model'; + import type { ScriptureReference, UnsubscriberAsync } from 'platform-bible-utils'; + import type { + DataProviderDataType, + DataProviderDataTypes, + DataProviderSubscriberOptions, + DataProviderUpdateInstructions, + } from '@shared/models/data-provider.model'; + import type { + MandatoryProjectDataTypes, + WithProjectDataProviderEngineExtensionDataMethods, + } from '@shared/models/project-data-provider.model'; + import type { + DefaultProjectStorageDataTypes, + MandatoryProjectStorageDataTypes, + } from '@shared/models/project-storage-interpreter.model'; import type { IDisposableDataProvider } from '@shared/models/data-provider.interface'; import type IDataProvider from '@shared/models/data-provider.interface'; import type ExtractDataProviderDataTypes from '@shared/models/extract-data-provider-data-types.model'; + // #region Commands + // TODO: Adding an index type removes type checking on the key :( How do we make sure extensions provide only functions? /** * Function types for each command available on the papi. Each extension can extend this interface * to add commands that it registers on the papi with `papi.commands.registerCommand`. * - * Note: Command names must consist of two string separated by at least one period. We recommend + * Note: Command names must consist of two strings separated by at least one period. We recommend * one period and lower camel case in case we expand the api in the future to allow dot notation. * * An extension can extend this interface to add types for the commands it registers by adding the @@ -53,37 +67,223 @@ declare module 'papi-shared-types' { */ export type CommandNames = keyof CommandHandlers; + // #endregion + + // #region User Settings + + /** + * Types corresponding to each user setting available in Platform.Bible. Keys are setting names, + * and values are setting data types. Extensions can add more user setting types with + * corresponding user setting IDs by adding details to their `.d.ts` file. + * + * Note: Setting names must consist of two strings separated by at least one period. We recommend + * one period and lower camel case in case we expand the api in the future to allow dot notation. + * + * An extension can extend this interface to add types for the user settings it registers by + * adding the following to its `.d.ts` file (in this example, we are adding the + * `myExtension.highlightColor` setting): + * + * @example + * + * ```typescript + * declare module 'papi-shared-types' { + * export interface SettingTypes { + * 'myExtension.highlightColor': string | { r: number; g: number; b: number }; + * } + * } + * ``` + */ export interface SettingTypes { 'platform.verseRef': ScriptureReference; 'platform.interfaceLanguage': string[]; } + /** + * Names for each user setting available on the papi. + * + * Automatically includes all extensions' user settings that are added to {@link SettingTypes}. + * + * @example 'platform.verseRef' + */ export type SettingNames = keyof SettingTypes; + // #endregion + + // #region Project Settings + + /** + * Types corresponding to each project setting available in Platform.Bible. Keys are project + * setting names, and values are project setting data types. Extensions can add more project + * setting types with corresponding project setting IDs by adding details to their `.d.ts` file. + * + * Note: Project setting names must consist of two strings separated by at least one period. We + * recommend one period and lower camel case in case we expand the api in the future to allow dot + * notation. + * + * An extension can extend this interface to add types for the project settings it registers by + * adding the following to its `.d.ts` file (in this example, we are adding the + * `myExtension.highlightColor` project setting): + * + * @example + * + * ```typescript + * declare module 'papi-shared-types' { + * export interface ProjectSettingTypes { + * 'myExtension.highlightColor': string | { r: number; g: number; b: number }; + * } + * } + * ``` + */ + export interface ProjectSettingTypes { + /** + * Localized name of the language in which this project is written. This will be displayed + * directly in the UI. + * + * @example 'English' + */ + 'platform.language': string; + /** + * Localized full name of the project. This will be displayed directly in the UI. + * + * @example 'World English Bible' + */ + 'platform.fullName': string; + } + + /** + * Names for each user setting available on the papi. + * + * Automatically includes all extensions' user settings that are added to + * {@link ProjectSettingTypes}. + * + * @example 'platform.fullName' + */ + export type ProjectSettingNames = keyof ProjectSettingTypes; + + // #endregion + + // #region Project Data Provider + + /** + * The `Setting` methods required for a Project Data Provider Engine to fulfill the requirements + * of {@link MandatoryProjectDataTypes}'s `Setting` data type. + */ + export type WithProjectDataProviderEngineSettingMethods< + TProjectDataTypes extends DataProviderDataTypes, + > = { + /** + * Set the value of the specified project setting on this project. + * + * @param key The string id of the project setting to change + * @param newSetting The value that is to be set to the project setting. + * @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 + */ + setSetting: ( + key: ProjectSettingName, + newSetting: ProjectSettingTypes[ProjectSettingName], + ) => Promise>; + /** + * Get the value of the specified project setting. + * + * Note: This is good for retrieving a project setting once. If you want to keep the value + * up-to-date, use `subscribeSetting` instead, which can immediately give you the value and keep + * it up-to-date. + * + * @param key The string id of the project setting to get + * @returns The value of the specified project setting. Returns default setting value if the + * project setting does not exist on the project. + * @throws If no default value is available for the setting. + */ + getSetting: ( + key: ProjectSettingName, + ) => Promise; + /** + * Deletes the specified project setting, setting it back to its default value. + * + * @param key The string id of the project setting to reset + * @returns `true` if successfully reset the project setting, `false` otherwise + */ + resetSetting: ( + key: ProjectSettingName, + ) => Promise; + }; + + /** + * An object on the papi that parses raw project data from a Project Storage Interpreter and has + * methods for interacting with that project data. Created by the papi and layers over an + * {@link IProjectDataProviderEngine} provided by an extension. Returned from getting a project + * data provider with `papi.projectDataProviders.get`. + * + * Project Data Providers are a specialized version of {@link IDataProvider} that works with a + * project of a specific `projectType`. For each project available, a new instance of a PDP with + * that project's `projectType` is created by the Project Data Provider Factory with that + * project's `projectType`. + * + * Every PDP **must** fulfill the requirements of all PDPs according to + * {@link MandatoryProjectDataTypes}. + * + * Note: Project Data Providers are associated to Project Storage Interpreters based on a shared + * `projectType`. A PDP must interact with its PSI according to the + * {@link ProjectStorageProjectTypes} exposed by the PSI for that `projectType`. + */ + export type IProjectDataProvider = IDataProvider< + TProjectDataTypes & MandatoryProjectDataTypes + > & + WithProjectDataProviderEngineSettingMethods & + WithProjectDataProviderEngineExtensionDataMethods & { + /** + * Subscribe to receive updates to the specified project setting. + * + * Note: By default, this `subscribeSetting` function automatically retrieves the current + * project setting value and runs the provided callback as soon as possible. That way, if you + * want to keep your data up-to-date, you do not also have to run `getSetting`. You can turn + * this functionality off in the `options` parameter. + * + * @param key The string id of the project setting for which to listen to changes + * @param callback Function to run with the updated project setting value + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber to stop listening for updates + */ + subscribeSetting: ( + key: ProjectSettingName, + callback: (value: ProjectSettingTypes[ProjectSettingName]) => void, + options: DataProviderSubscriberOptions, + ) => Promise; + }; + /** This is just a simple example so we have more than one. It's not intended to be real. */ - export type NotesOnlyProjectDataTypes = MandatoryProjectDataType & { + export type NotesOnlyProjectDataTypes = MandatoryProjectDataTypes & { Notes: DataProviderDataType; }; /** - * `IDataProvider` types for each project data provider supported by PAPI. Extensions can add more - * project data providers with corresponding data provider IDs by adding details to their `.d.ts` - * file. Note that all project data types should extend `MandatoryProjectDataTypes` like the - * following example. + * {@link IProjectDataProvider} types for each `projectType` supported by PAPI. Extensions can add + * more Project Data Providers with corresponding `projectType`s by adding details to their + * `.d.ts` file and registering a Project Data Provider factory with the corresponding + * `projectType`. Note that all Project Data Providers' data types should extend + * {@link MandatoryProjectDataTypes} like the following example. * - * Note: Project Data Provider names must consist of two string separated by at least one period. - * We recommend one period and lower camel case in case we expand the api in the future to allow - * dot notation. + * Note: The keys of this interface are the `projectType`s for the associated Project Data + * Providers. * - * An extension can extend this interface to add types for the project data provider it registers - * by adding the following to its `.d.ts` file (in this example, we are adding the - * `MyExtensionProjectTypeName` data provider types): + * Note: Project Data Providers are associated to Project Storage Interpreters based on a shared + * `projectType`. {@link ProjectStorageInterpreters} is sometimes indexed by {@link ProjectTypes}, + * so please make PSIs available to support the PDPs available. We recommend you specify a Project + * Storage Interpreter type on {@link ProjectStorageInterpreters} for each `projectType` for which + * you add a PDP type here in order to indicate what interface you expect to interact with in your + * PDP. + * + * An extension can extend this interface to add types for the Project Data Providers its + * registered factory provides by adding the following to its `.d.ts` file (in this example, we + * are adding a Project Data Provider type for the `MyExtensionProjectTypeName` `projectType`): * * @example * * ```typescript * declare module 'papi-shared-types' { - * export type MyProjectDataType = MandatoryProjectDataType & { + * export type MyProjectDataType = MandatoryProjectDataTypes & { * MyProjectData: DataProviderDataType; * }; * @@ -94,31 +294,39 @@ declare module 'papi-shared-types' { * ``` */ export interface ProjectDataProviders { - 'platform.notesOnly': IDataProvider; - 'platform.placeholder': IDataProvider; + 'platform.notesOnly': IProjectDataProvider; + 'platform.placeholder': IProjectDataProvider; } /** - * Names for each project data provider available on the papi. + * Names for each `projectType` available on the papi. Each of the `projectType`s should have a + * registered Project Data Provider Factory that provides Project Data Providers for the + * `projectType` along with one or more Project Storage Interpreters for the `projectType`. * - * Automatically includes all extensions' project data providers that are added to + * Automatically includes all extensions' `projectTypes` that are added to * {@link ProjectDataProviders}. * - * @example 'platform.placeholder' + * Note: {@link ProjectStorageInterpreters} is sometimes indexed by {@link ProjectTypes}, so please + * make PSIs available to support the PDPs available. + * + * @example 'platform.notesOnly' */ export type ProjectTypes = keyof ProjectDataProviders; /** - * `DataProviderDataTypes` for each project data provider supported by PAPI. These are the data - * types served by each project data provider. + * `DataProviderDataTypes` for each Project Data Provider supported by PAPI. These are the data + * types served by Project Data Providers for each `projectType`. * - * Automatically includes all extensions' project data providers that are added to + * Automatically includes all extensions' `projectTypes` that are added to * {@link ProjectDataProviders}. * + * Note: The keys of this interface are the `projectType`s for the associated project data + * provider data types. + * * @example * * ```typescript - * ProjectDataTypes['MyExtensionProjectTypeName'] => { + * ProjectDataTypes['MyExtensionProjectTypeName'] => MandatoryProjectDataTypes & { * MyProjectData: DataProviderDataType; * } * ``` @@ -127,6 +335,198 @@ declare module 'papi-shared-types' { [ProjectType in ProjectTypes]: ExtractDataProviderDataTypes; }; + // #endregion + + // #region Project Storage Interpreter + + /** + * Indicates to a Project Storage Interpreter what project setting is being referenced on what + * project. Generally, a Project Data Provider passes calls to `Setting` data type methods to its + * PSI and adds the `projectId`. + */ + export type ProjectStorageSettingDataScope = { + /** Key of the Project Setting to select */ + key: ProjectSettingName; + /** ID for the project whose extension data to get */ + projectId: string; + }; + + /** + * An object on the papi that manages raw project data and has methods for a Project Data Provider + * to interact with that raw project data. Created by the papi and layers over an + * {@link IProjectStorageInterpreterEngine} provided by an extension. + * + * Project Storage Interpreters are a specialized version of {@link IDataProvider} that works with + * projects of a specific `storageType` and one or more `projectType`s. For each project + * available, a PDP with that project's `projectType` will interact with the PSI with that + * project's `storageType` and `projectType`. + * + * Every PSI **must** fulfill the requirements of all PSIs according to + * {@link MandatoryProjectStorageDataTypes}. + * + * Note: Project Data Providers are associated to Project Storage Interpreters based on a shared + * `projectType`. A PSI must implement the {@link ProjectStorageProjectTypes} specified for each + * `projectType` it supports. + * + * Using this interface without specifying data types will default to using + * {@link DefaultProjectStorageDataTypes} as a sensible default method of communication between a + * PDP and a PSI for a specific `projectType`. + */ + export type IProjectStorageInterpreter< + TProjectStorageDataTypes extends DataProviderDataTypes = DefaultProjectStorageDataTypes, + > = IDataProvider & + IDataProvider & { + /** + * Set the value of the specified project setting on this project. + * + * @param settingDataScope The string id of the project setting to change and the project on + * which to change it + * @param newSetting The value that is to be set to the project setting. + * @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 + */ + setSetting: ( + settingDataScope: ProjectStorageSettingDataScope, + newSetting: ProjectSettingTypes[ProjectSettingName], + ) => Promise>; + /** + * Get the value of the specified project setting. + * + * Note: This is good for retrieving a project setting once. If you want to keep the value + * up-to-date, use `subscribeSetting` instead, which can immediately give you the value and + * keep it up-to-date. + * + * @param settingDataScope The string id of the project setting to get and the project from + * which to get it + * @returns The value of the specified project setting. Returns default setting value if the + * project setting does not exist on the project. + * @throws If no default value is available for the setting. + */ + getSetting: ( + settingDataScope: ProjectStorageSettingDataScope, + ) => Promise; + /** + * Subscribe to receive updates to the specified project setting. + * + * Note: By default, this `subscribeSetting` function automatically retrieves the current + * project setting value and runs the provided callback as soon as possible. That way, if you + * want to keep your data up-to-date, you do not also have to run `getSetting`. You can turn + * this functionality off in the `options` parameter. + * + * @param settingDataScope The string id of the project setting for which to listen to changes + * and the project on which to listen + * @param callback Function to run with the updated project setting value + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber to stop listening for updates + */ + subscribeSetting: ( + settingDataScope: ProjectStorageSettingDataScope, + callback: (value: ProjectSettingTypes[ProjectSettingName]) => void, + options: DataProviderSubscriberOptions, + ) => Promise; + /** + * Deletes the specified project setting, setting it back to its default value. + * + * @param settingDataScope The string id of the project setting to reset and the project on + * which to reset it + * @returns `true` if successfully reset the project setting, `false` otherwise + */ + resetSetting: ( + settingDataScope: ProjectStorageSettingDataScope, + ) => Promise; + }; + + /** + * {@link IProjectStorageInterpreter} types for each `projectType` supported by PAPI. Extensions + * can add more Project Storage Interpreters that support corresponding `projectType`s by adding + * details to their `.d.ts` file and registering a Project Storage Interpreter that supports the + * corresponding `projectType`. Note that all Project Storage Interpreters' data types should + * extend {@link MandatoryProjectStorageDataTypes} like the following example. + * + * Note: The keys of this interface are the `projectType`s supported by available Project Storage + * Interpreters. + * + * WARNING: Each Project Storage Interpreter **must** fulfill certain requirements for its + * `getSetting`, `setSetting`, and `resetSetting` methods. See + * {@link MandatoryProjectStorageDataTypes} for more information. + * + * An extension can extend this interface to add types for the `projectType`s its Project Storage + * Interpreters support by adding the following to its `.d.ts` file (in this example, we are + * adding a Project Storage Interpreter type for the `MyExtensionProjectTypeName` `projectType`): + * + * @example + * + * ```typescript + * declare module 'papi-shared-types' { + * export type MyProjectStorageDataType = MandatoryProjectStorageDataTypes & { + * ProjectData: DataProviderDataType< + * { projectId: string; section: number }, + * string | undefined, + * string + * >; + * }; + * + * export interface ProjectStorageInterpreters { + * MyExtensionProjectTypeName: IProjectStorageInterpreter; + * } + * } + * ``` + */ + export interface ProjectStorageInterpreters { + 'platform.notesOnly': IProjectStorageInterpreter; + 'platform.placeholder': IProjectStorageInterpreter; + } + + /** + * Names for each `projectType` supported by available Project Storage Interpreters on the papi. + * Each of the `projectType`s should have a registered Project Data Provider Factory that provides + * Project Data Providers for the `projectType` along with one or more Project Storage + * Interpreters for the `projectType`. + * + * Automatically includes all extensions' `projectTypes` that are added to + * {@link ProjectStorageInterpreters}. + * + * Note: {@link ProjectStorageInterpreters} is sometimes indexed by {@link ProjectTypes}, so please + * make PSIs available to support the PDPs available. + * + * @example 'platform.notesOnly' + */ + export type ProjectStorageProjectTypes = keyof ProjectStorageInterpreters; + + /** + * `DataProviderDataTypes` for each Project Storage Interpreter supported by PAPI. These are the + * data types served by Project Storage Interpreters to Project Data Providers for each + * `projectType`. + * + * Automatically includes all extensions' `projectTypes` that are added to + * {@link ProjectStorageInterpreters}. + * + * Note: The keys of this interface are the `projectType`s supported by available Project Storage + * Interpreters. + * + * @example + * + * ```typescript + * ProjectStorageDataTypes['MyExtensionProjectTypeName'] => MandatoryProjectStorageDataTypes & { + * ProjectData: DataProviderDataType< + * { projectId: string; section: number }, + * string | undefined, + * string + * >; + * } + * ``` + */ + export type ProjectStorageDataTypes = { + [ProjectType in ProjectStorageProjectTypes]: ExtractDataProviderDataTypes< + ProjectStorageInterpreters[ProjectType] + >; + }; + + // #endregion + + // #region Data Provider + type StuffDataTypes = { Stuff: DataProviderDataType }; type PlaceholderDataTypes = { @@ -134,12 +534,12 @@ declare module 'papi-shared-types' { }; /** - * `IDataProvider` types for each data provider supported by PAPI. Extensions can add more data - * providers with corresponding data provider IDs by adding details to their `.d.ts` file and + * {@link IDataProvider} types for each data provider supported by PAPI. Extensions can add more + * data providers with corresponding data provider IDs by adding details to their `.d.ts` file and * registering a data provider engine in their `activate` function with * `papi.dataProviders.registerEngine`. * - * Note: Data Provider names must consist of two string separated by at least one period. We + * Note: Data Provider names must consist of two strings separated by at least one period. We * recommend one period and lower camel case in case we expand the api in the future to allow dot * notation. * @@ -218,4 +618,6 @@ declare module 'papi-shared-types' { DataProviders[DataProviderName] >; }; + + // #endregion } diff --git a/src/extension-host/services/menu-data.service-host.ts b/src/extension-host/services/menu-data.service-host.ts index 883ae34de6..44ba465a7e 100644 --- a/src/extension-host/services/menu-data.service-host.ts +++ b/src/extension-host/services/menu-data.service-host.ts @@ -4,8 +4,8 @@ import { menuDataServiceObjectToProxy, menuDataServiceProviderName, } from '@shared/services/menu-data.service-model'; -import dataProviderService, { DataProviderEngine } from '@shared/services/data-provider.service'; -import IDataProviderEngine from '@shared/models/data-provider-engine.model'; +import dataProviderService from '@shared/services/data-provider.service'; +import IDataProviderEngine, { DataProviderEngine } from '@shared/models/data-provider-engine.model'; import { DataProviderUpdateInstructions } from '@shared/models/data-provider.model'; import { createSyncProxyForAsyncObject, diff --git a/src/extension-host/services/papi-backend.service.ts b/src/extension-host/services/papi-backend.service.ts index 88f32c4119..144b059208 100644 --- a/src/extension-host/services/papi-backend.service.ts +++ b/src/extension-host/services/papi-backend.service.ts @@ -14,10 +14,8 @@ import { PapiWebViewProviderService, } from '@shared/services/web-view-provider.service'; import internetService, { InternetService } from '@shared/services/internet.service'; -import dataProviderService, { - DataProviderService, - DataProviderEngine as PapiDataProviderEngine, -} from '@shared/services/data-provider.service'; +import dataProviderService, { DataProviderService } from '@shared/services/data-provider.service'; +import { DataProviderEngine as PapiDataProviderEngine } from '@shared/models/data-provider-engine.model'; import { papiBackendProjectDataProviderService, PapiBackendProjectDataProviderService, @@ -33,6 +31,9 @@ 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'; +import projectSettingsService from '@shared/services/project-settings.service'; +import { IProjectSettingsService } from '@shared/services/project-settings.service-model'; +import { ProjectDataProviderEngine as PapiProjectDataProviderEngine } from '@shared/models/project-data-provider-engine.model'; // IMPORTANT NOTES: // 1) When adding new services here, consider whether they also belong in papi-frontend.service.ts. @@ -46,6 +47,8 @@ const papi = { // Classes /** JSDOC DESTINATION DataProviderEngine */ DataProviderEngine: PapiDataProviderEngine, + /** JSDOC DESTINATION ProjectDataProviderEngine */ + ProjectDataProviderEngine: PapiProjectDataProviderEngine, // Functions /** This is just an alias for internet.fetch */ @@ -73,6 +76,8 @@ const papi = { papiBackendProjectDataProviderService as PapiBackendProjectDataProviderService, /** JSDOC DESTINATION projectLookupService */ projectLookup: projectLookupService as ProjectLookupServiceType, + /** JSDOC DESTINATION projectSettingsService */ + projectSettings: projectSettingsService as IProjectSettingsService, /** JSDOC DESTINATION extensionStorageService */ storage: extensionStorageService as ExtensionStorageService, /** JSDOC DESTINATION settingsService */ @@ -92,6 +97,9 @@ export default papi; /** JSDOC DESTINATION DataProviderEngine */ export const { DataProviderEngine } = papi; Object.freeze(papi.DataProviderEngine); +/** JSDOC DESTINATION ProjectDataProviderEngine */ +export const { ProjectDataProviderEngine } = papi; +Object.freeze(papi.ProjectDataProviderEngine); /** This is just an alias for internet.fetch */ export const { fetch } = papi; Object.freeze(papi.fetch); @@ -125,6 +133,9 @@ Object.freeze(papi.projectDataProviders); /** JSDOC DESTINATION projectLookupService */ export const { projectLookup } = papi; Object.freeze(papi.projectLookup); +/** JSDOC DESTINATION projectSettingsService */ +export const { projectSettings } = papi; +Object.freeze(papi.projectSettings); /** JSDOC DESTINATION extensionStorageService */ export const { storage } = papi; Object.freeze(papi.storage); diff --git a/src/extension-host/services/project-lookup.service-host.ts b/src/extension-host/services/project-lookup.service-host.ts index f0967c28b3..828f7e2087 100644 --- a/src/extension-host/services/project-lookup.service-host.ts +++ b/src/extension-host/services/project-lookup.service-host.ts @@ -86,6 +86,7 @@ async function reloadMetadata(): Promise { } let initializationPromise: Promise; +/** Do the setup this service needs to function */ async function initialize(): Promise { if (!initializationPromise) { initializationPromise = new Promise((resolve, reject) => { diff --git a/src/main/data/core-project-settings-info.data.ts b/src/main/data/core-project-settings-info.data.ts new file mode 100644 index 0000000000..94843ebb45 --- /dev/null +++ b/src/main/data/core-project-settings-info.data.ts @@ -0,0 +1,30 @@ +import { ProjectSettingNames, ProjectSettingTypes } from 'papi-shared-types'; + +/** Information about one project setting */ +type ProjectSettingInfo = { + default: ProjectSettingTypes[ProjectSettingName]; +}; + +/** + * Information about all project settings. Keys are project setting keys, values are information for + * that project setting + */ +export type AllProjectSettingsInfo = { + [ProjectSettingName in ProjectSettingNames]: ProjectSettingInfo; +}; + +/** Info about all project settings built into core. Does not contain info for extensions' settings */ +const coreProjectSettingsInfo: Partial = { + 'platform.fullName': { default: '%project_full_name_missing%' }, + 'platform.language': { default: '%project_language_missing%' }, + 'platformScripture.booksPresent': { + default: + // 1 + // 1 2 3 4 5 6 7 8 9 0 1 2 3 + // 34567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 + '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + }, + 'platformScripture.versification': { default: 4 }, +}; + +export default coreProjectSettingsInfo; diff --git a/src/main/main.ts b/src/main/main.ts index 050f30a148..3e98fa0b8a 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -25,9 +25,10 @@ import { SerializedRequestType } from '@shared/utils/util'; import networkObjectStatusService from '@shared/services/network-object-status.service'; 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'; +import { startNetworkObjectStatusService } from '@main/services/network-object-status.service-host'; +import { startLocalizationService } from '@main/services/localization.service-host'; +import { initialize as initializeSettingsService } from '@main/services/settings.service-host'; +import { startProjectSettingsService } from '@main/services/project-settings.service-host'; const PROCESS_CLOSE_TIME_OUT = 2000; @@ -85,6 +86,8 @@ async function main() { await initializeSettingsService(); + await startProjectSettingsService(); + // 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 diff --git a/src/main/services/localization.service-host.test.tsx b/src/main/services/localization.service-host.test.ts similarity index 100% rename from src/main/services/localization.service-host.test.tsx rename to src/main/services/localization.service-host.test.ts diff --git a/src/main/services/localization.service-host.ts b/src/main/services/localization.service-host.ts index 702792773d..8ea6e747d0 100644 --- a/src/main/services/localization.service-host.ts +++ b/src/main/services/localization.service-host.ts @@ -59,6 +59,7 @@ async function loadAllLocalizationData(): Promise> } let initializationPromise: Promise; +/** Do the setup this service needs to function */ async function initialize(): Promise { if (!initializationPromise) { initializationPromise = new Promise((resolve, reject) => { diff --git a/src/main/services/project-settings.service-host.test.ts b/src/main/services/project-settings.service-host.test.ts new file mode 100644 index 0000000000..92fdbbf481 --- /dev/null +++ b/src/main/services/project-settings.service-host.test.ts @@ -0,0 +1,44 @@ +import { testingProjectSettingsService } from '@main/services/project-settings.service-host'; + +jest.mock('@main/data/core-project-settings-info.data', () => ({ + __esModule: true, + default: { + 'platform.fullName': { default: '%test_project_full_name_missing%' }, + 'platform.language': { default: '%test_project_language_missing%' }, + 'platformScripture.booksPresent': { + default: 'thisIsNotActuallyBooksPresent', + }, + // Not present! Should throw error 'platformScripture.versification': { default: 1629326 }, + }, +})); + +describe('isValid', () => { + it('should return true always - TEMP. TODO: Fix when we implement validation #511', async () => { + const isSettingChangeValid = await testingProjectSettingsService.isValid( + '', + '', + 'platform.fullName', + {}, + 'ParatextStandard', + ); + expect(isSettingChangeValid).toBe(true); + }); +}); + +describe('getDefault', () => { + it('should get default value', async () => { + const projectSettingKey = 'platform.fullName'; + const defaultValue = await testingProjectSettingsService.getDefault( + projectSettingKey, + 'ParatextStandard', + ); + expect(defaultValue).toBe('%test_project_full_name_missing%'); + }); + + it('should throw if a default is not present', async () => { + const projectSettingKey = 'platformScripture.versification'; + await expect( + testingProjectSettingsService.getDefault(projectSettingKey, 'ParatextStandard'), + ).rejects.toThrow(new RegExp(`default value for project setting ${projectSettingKey}`)); + }); +}); diff --git a/src/main/services/project-settings.service-host.ts b/src/main/services/project-settings.service-host.ts new file mode 100644 index 0000000000..2a8348492f --- /dev/null +++ b/src/main/services/project-settings.service-host.ts @@ -0,0 +1,78 @@ +import coreProjectSettingsInfo, { + AllProjectSettingsInfo, +} from '@main/data/core-project-settings-info.data'; +import networkObjectService from '@shared/services/network-object.service'; +import { + IProjectSettingsService, + projectSettingsServiceNetworkObjectName, +} from '@shared/services/project-settings.service-model'; +import { ProjectSettingNames, ProjectSettingTypes, ProjectTypes } from 'papi-shared-types'; + +/** + * Our internal list of project settings information for each setting. Theoretically this should not + * be partial, but it quite possibly will not have each setting in it. It just depends on if + * extensions actually provide the settings definitions + */ +let projectSettingsInfo: Partial; + +let initializationPromise: Promise; +/** Do the setup this service needs to function */ +async function initialize(): Promise { + if (!initializationPromise) { + initializationPromise = new Promise((resolve, reject) => { + const executor = async () => { + try { + projectSettingsInfo = coreProjectSettingsInfo; + // TODO: Read projectSettingsInfo in from extensions in https://github.com/paranext/paranext-core/issues/721 + resolve(); + } catch (error) { + reject(error); + } + }; + executor(); + }); + } + return initializationPromise; +} + +// TODO: Implement validators in https://github.com/paranext/paranext-core/issues/511 +async function isValid(): Promise { + return true; +} + +async function getDefault( + key: ProjectSettingName, + projectType: ProjectTypes, +): Promise { + await initialize(); + const projectSettingInfo = projectSettingsInfo[key]; + + if (!projectSettingInfo || !('default' in projectSettingInfo)) + throw new Error( + `Could not find default value for project setting ${key}. projectType: ${projectType}`, + ); + + return projectSettingInfo.default; +} + +const projectSettingsService: IProjectSettingsService = { + isValid, + getDefault, +}; + +/** This is an internal-only export for testing purposes, and should not be used in development */ +export const testingProjectSettingsService = { + ...projectSettingsService, +}; + +/** Register the network object that backs the PAPI localization service */ +// This doesn't really represent this service module, so we're not making it default. To use this +// service, you should use `localization.service.ts` +// eslint-disable-next-line import/prefer-default-export +export async function startProjectSettingsService(): Promise { + await initialize(); + await networkObjectService.set( + projectSettingsServiceNetworkObjectName, + projectSettingsService, + ); +} diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index b4ea64c7b3..cd8ff44b30 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -1,9 +1,9 @@ -import IDataProviderEngine from '@shared/models/data-provider-engine.model'; +import IDataProviderEngine, { DataProviderEngine } from '@shared/models/data-provider-engine.model'; import { DataProviderDataType, DataProviderUpdateInstructions, } from '@shared/models/data-provider.model'; -import dataProviderService, { DataProviderEngine } from '@shared/services/data-provider.service'; +import dataProviderService from '@shared/services/data-provider.service'; import { AllSettingsData, ISettingsService, diff --git a/src/renderer/hooks/papi-hooks/index.ts b/src/renderer/hooks/papi-hooks/index.ts index 77b1849292..c38979095a 100644 --- a/src/renderer/hooks/papi-hooks/index.ts +++ b/src/renderer/hooks/papi-hooks/index.ts @@ -3,5 +3,6 @@ export { default as useData } from '@renderer/hooks/papi-hooks/use-data.hook'; export { default as useSetting } from '@renderer/hooks/papi-hooks/use-setting.hook'; export { default as useProjectData } from '@renderer/hooks/papi-hooks/use-project-data.hook'; export { default as useProjectDataProvider } from '@renderer/hooks/papi-hooks/use-project-data-provider.hook'; +export { default as useProjectSetting } from '@renderer/hooks/papi-hooks/use-project-setting.hook'; export { default as useDialogCallback } from '@renderer/hooks/papi-hooks/use-dialog-callback.hook'; export { default as useDataProviderMulti } from '@renderer/hooks/papi-hooks/use-data-provider-multi.hook'; diff --git a/src/renderer/hooks/papi-hooks/use-data.hook.ts b/src/renderer/hooks/papi-hooks/use-data.hook.ts index f116a5812e..4802755619 100644 --- a/src/renderer/hooks/papi-hooks/use-data.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-data.hook.ts @@ -95,9 +95,9 @@ type UseDataHook = { * _@returns_ `[data, setData, isLoading]` * * - `data`: the current value for the data from the data provider with the specified data type and - * selector, either the defaultValue or the resolved data + * selector, either the `defaultValue` or the resolved data * - `setData`: asynchronous function to request that the data provider update the data at this data - * type and selector. Returns true if successful. Note that this function does not update the + * type and selector. Returns `true` if successful. Note that this function does not update the * data. The data provider sends out an update to this subscription if it successfully updates * data. * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the data diff --git a/src/renderer/hooks/papi-hooks/use-project-data-provider.hook.ts b/src/renderer/hooks/papi-hooks/use-project-data-provider.hook.ts index dd7e6d5d99..b6119aa8c9 100644 --- a/src/renderer/hooks/papi-hooks/use-project-data-provider.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-project-data-provider.hook.ts @@ -7,13 +7,13 @@ import createUseNetworkObjectHook from '@renderer/hooks/hook-generators/create-u * with those parameters. * * @param projectType Indicates what you expect the `projectType` to be for the project with the - * specified id. The TypeScript type for the returned project data provider will have the project - * data provider type associated with this project type. If this argument does not match the + * specified id. The TypeScript type for the returned Project Data Provider will have the Project + * Data Provider type associated with this `projectType`. If this argument does not match the * project's actual `projectType` (according to its metadata), a warning will be logged * @param projectDataProviderSource String name of the id of the project to get OR * projectDataProvider (result of useProjectDataProvider, if you want this hook to just return the * data provider again) - * @returns `projectDataProviderSource` for getting the project data provider + * @returns `projectDataProviderSource` for getting the Project Data Provider */ function mapParametersToProjectDataProviderSource( _projectType: ProjectType, @@ -26,15 +26,15 @@ function mapParametersToProjectDataProviderSource', '').` and use like any other React * hook. * @@ -84,13 +84,13 @@ type UseProjectDataHook = { * ``` * * _@param_ `projectType` Indicates what you expect the `projectType` to be for the project with the - * specified id. The TypeScript type for the returned project data provider will have the project - * data provider type associated with this project type. If this argument does not match the + * specified id. The TypeScript type for the returned Project Data Provider will have the Project + * Data Provider type associated with this project type. If this argument does not match the * project's actual `projectType` (according to its metadata), a warning will be logged * * _@param_ `projectDataProviderSource` String name of the id of the project to get OR * projectDataProvider (result of useProjectDataProvider if you want to consolidate and only get the - * project data provider once) + * Project Data Provider once) * * _@param_ `selector` tells the provider what data this listener is listening for * @@ -102,11 +102,20 @@ type UseProjectDataHook = { * _@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. This means that `subscriberOptions` will be passed to the project - * data provider's `subscribe` method as soon as possible and will not be updated again + * to re-run with its new value. This means that `subscriberOptions` will be passed to the Project + * Data Provider's `subscribe` method as soon as possible and will not be updated again * until `projectDataProviderSource` or `selector` changes. * * _@returns_ `[data, setData, isLoading]` + * + * - `data`: the current value for the data from the Project Data Provider with the specified data + * type and selector, either the `defaultValue` or the resolved data + * - `setData`: asynchronous function to request that the Project Data Provider update the data at + * this data type and selector. Returns `true` if successful. Note that this function does not + * update the data. The Project Data Provider sends out an update to this subscription if it + * successfully updates data. + * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the data + * provider */ // Assert the more general and more specific types. /* eslint-disable no-type-assertion/no-type-assertion */ diff --git a/src/renderer/hooks/papi-hooks/use-project-setting.hook.ts b/src/renderer/hooks/papi-hooks/use-project-setting.hook.ts new file mode 100644 index 0000000000..fbf727a086 --- /dev/null +++ b/src/renderer/hooks/papi-hooks/use-project-setting.hook.ts @@ -0,0 +1,112 @@ +import { useMemo } from 'react'; +import { + ProjectDataProviders, + ProjectDataTypes, + ProjectSettingNames, + ProjectSettingTypes, + ProjectTypes, +} from 'papi-shared-types'; +import useProjectData from '@renderer/hooks/papi-hooks/use-project-data.hook'; +import { + DataProviderSubscriberOptions, + DataProviderUpdateInstructions, +} from '@shared/models/data-provider.model'; +import ExtractDataProviderDataTypes from '@shared/models/extract-data-provider-data-types.model'; +import useProjectDataProvider from './use-project-data-provider.hook'; + +/** + * Gets, sets and resets a project setting on the papi for a specified project. Also notifies + * subscribers when the project setting changes and gets updated when the project setting is changed + * by others. + * + * @param projectType Indicates what you expect the `projectType` to be for the project with the + * specified id. The TypeScript type for the returned Project Data Provider will have the Project + * Data Provider type associated with this `projectType`. If this argument does not match the + * project's actual `projectType` (according to its metadata), a warning will be logged + * @param projectDataProviderSource `projectDataProviderSource` String name of the id of the project + * to get OR projectDataProvider (result of `useProjectDataProvider` if you want to consolidate + * and only get the Project Data Provider once) + * @param key The string id of the project setting to interact with + * + * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be + * updated every render + * @param defaultValue The initial value to return while first awaiting the project 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. This means that `subscriberOptions` will be passed to the data + * provider's `subscribe` method as soon as possible and will not be updated again + * until `dataProviderSource` or `selector` changes. + * @returns `[setting, setSetting, resetSetting]` + * + * - `setting`: the current value for the project setting from the Project Data Provider with the + * specified key, either the `defaultValue` or the resolved setting value + * - `setSetting`: asynchronous function to request that the Project Data Provider update the project + * setting with the specified key. Returns `true` if successful. Note that this function does + * not update the data. The Project Data Provider sends out an update to this subscription if + * it successfully updates data. + * - `resetSetting`: asynchronous function to request that the Project Data Provider reset the project + * setting + * - `isLoading`: whether the setting value is awaiting retrieval from the Project Data Provider + * + * @throws When subscription callback function is called with an update that has an unexpected + * message type + */ +const useProjectSetting = < + ProjectType extends ProjectTypes, + ProjectSettingName extends ProjectSettingNames, +>( + projectType: ProjectType, + projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined, + key: ProjectSettingName, + defaultValue: ProjectSettingTypes[ProjectSettingName], + subscriberOptions?: DataProviderSubscriberOptions, +): [ + setting: ProjectSettingTypes[ProjectSettingName], + setSetting: ((newSetting: ProjectSettingTypes[ProjectSettingName]) => void) | undefined, + resetSetting: (() => void) | undefined, + isLoading: boolean, +] => { + const projectDataProvider = useProjectDataProvider(projectType, projectDataProviderSource); + + // Unfortunately `Setting` isn't an actual `DataProviderDataType` on Project Data Providers. They + // instead manually implement on `IProjectDataProvider` with a generic `ProjectSettingName`. + // We must type assert to use it because `useProjectData` only sees actual `DataProviderDataType`s + /* eslint-disable no-type-assertion/no-type-assertion */ + const [setting, setSetting, isLoading] = ( + useProjectData(projectType, projectDataProvider) as { + Setting: ( + selector: ProjectSettingName, + defaultValue: ProjectSettingTypes[ProjectSettingName], + subscriberOptions?: DataProviderSubscriberOptions, + ) => [ + setting: ProjectSettingTypes[ProjectSettingName], + setSetting: + | (( + newData: ProjectSettingTypes[ProjectSettingName], + ) => Promise< + DataProviderUpdateInstructions< + ExtractDataProviderDataTypes + > + >) + | undefined, + boolean, + ]; + } + ).Setting(key, defaultValue, subscriberOptions); + /* eslint-enable */ + + const resetSetting = useMemo( + () => + projectDataProvider + ? () => { + projectDataProvider.resetSetting(key); + } + : undefined, + [projectDataProvider, key], + ); + + return [setting, setSetting, resetSetting, isLoading]; +}; + +export default useProjectSetting; diff --git a/src/shared/models/data-provider-engine.model.ts b/src/shared/models/data-provider-engine.model.ts index a633b66141..fa944c48d5 100644 --- a/src/shared/models/data-provider-engine.model.ts +++ b/src/shared/models/data-provider-engine.model.ts @@ -6,38 +6,44 @@ import { } from '@shared/models/data-provider.model'; import { NetworkableObject } from '@shared/models/network-object.model'; +// Note: the following comment uses @, not the actual @ character, to hackily provide @param and +// such on this object. JSDoc does not usually carry these to classes inheriting from +// `DataProviderEngine` for some reason. One day, we may be able to put this comment on an actual +// function, so we can fix the comments back to using real @ /** * JSDOC SOURCE DataProviderEngineNotifyUpdate * * Method to run to send clients updates for a specific data type outside of the `set` - * method. papi overwrites this function on the DataProviderEngine itself to emit an update after - * running the `notifyUpdate` method in the DataProviderEngine. + * method. papi overwrites this function on the DataProviderEngine itself to emit an update based on + * the `updateInstructions` and then run the original `notifyUpdateMethod` from the + * `DataProviderEngine`. * - * @example To run `notifyUpdate` function so it updates the Verse and Heresy data types (in a data - * provider engine): + * _@example_ To run `notifyUpdate` function so it updates the Verse and Heresy data types (in a + * data provider engine): * * ```typescript * this.notifyUpdate(['Verse', 'Heresy']); * ``` * - * @example You can log the manual updates in your data provider engine by specifying the following - * `notifyUpdate` function in the data provider engine: + * _@example_ You can log the manual updates in your data provider engine by specifying the + * following `notifyUpdate` function in the data provider engine: * * ```typescript * notifyUpdate(updateInstructions) { - * papi.logger.info(updateInstructions); + * papi.logger.info(updateInstructions); * } * ``` * * Note: This function's return is treated the same as the return from `set` * - * @param updateInstructions Information that papi uses to interpret whether to send out updates. - * Defaults to `'*'` (meaning send updates for all data types) if parameter `updateInstructions` - * is not provided or is undefined. Otherwise returns `updateInstructions`. papi passes the - * interpreted update value into this `notifyUpdate` function. For example, running - * `this.notifyUpdate()` will call the data provider engine's `notifyUpdate` with - * `updateInstructions` of `'*'`. - * @see DataProviderUpdateInstructions for more info on the `updateInstructions` parameter + * _@param_ `updateInstructions` Information that papi uses to interpret whether to send out + * updates. Defaults to `'*'` (meaning send updates for all data types) if parameter + * `updateInstructions` is not provided or is undefined. Otherwise returns `updateInstructions`. + * papi passes the interpreted update value into this `notifyUpdate` function. For example, running + * `this.notifyUpdate()` will call the data provider engine's `notifyUpdate` with + * `updateInstructions` of `'*'`. + * + * _@see_ {@link DataProviderUpdateInstructions} for more info on the `updateInstructions` parameter * * WARNING: Do not update a data type in its `get` method (unless you make a base case)! * It will create a destructive infinite loop. @@ -51,8 +57,8 @@ export type DataProviderEngineNotifyUpdate = { /** JSDOC DESTINATION DataProviderEngineNotifyUpdate */ @@ -61,13 +67,13 @@ export type WithNotifyUpdate = { /** * The object to register with the DataProviderService to create a data provider. The - * DataProviderService creates an IDataProvider on the papi that layers over this engine, providing - * special functionality. + * DataProviderService creates an {@link IDataProvider} on the papi that layers over this engine, + * providing special functionality. * * @type TDataTypes - The data types that this data provider engine serves. For each data type * defined, the engine must have corresponding `get` and `set function` * functions. - * @see DataProviderDataTypes for information on how to make powerful types that work well with + * @see {@link DataProviderDataTypes} for information on how to make powerful types that work well with * Intellisense. * * Note: papi creates a `notifyUpdate` function on the data provider engine if one is not provided, so it @@ -75,8 +81,16 @@ export type WithNotifyUpdate = { * not understand that papi will create one as you are writing your data provider engine, so you can * avoid type errors with one of the following options: * - * 1. If you are using an object or class to create a data provider engine, you can add a - * `notifyUpdate` function (and, with an object, add the WithNotifyUpdate type) to + * 1. If you are using a class to create a data provider engine, you can extend the + * {@link DataProviderEngine} class, and it will provide `notifyUpdate` for you: + * ```typescript + * class MyDPE extends DataProviderEngine implements IDataProviderEngine { + * ... + * } + * ``` + * + * 2. If you are using an object or class to create a data provider engine, you can add a + * `notifyUpdate` function (and, with an object, add the {@link WithNotifyUpdate} type) to * your data provider engine like so: * ```typescript * const myDPE: IDataProviderEngine & WithNotifyUpdate = { @@ -91,14 +105,6 @@ export type WithNotifyUpdate = { * ... * } * ``` - * - * 2. If you are using a class to create a data provider engine, you can extend the `DataProviderEngine` - * class, and it will provide `notifyUpdate` for you: - * ```typescript - * class MyDPE extends DataProviderEngine implements IDataProviderEngine { - * ... - * } - * ``` */ // Try using DataProviderName here instead of TDataTypes? type IDataProviderEngine = @@ -117,7 +123,7 @@ type IDataProviderEngine` method! It will create as * many updates as you run `set` methods. * - * @see DataProviderSetter for more information + * @see {@link DataProviderSetter} for more information */ DataProviderSetters & /** @@ -127,9 +133,29 @@ type IDataProviderEngine` method has a corresponding `get` * method. * - * @see DataProviderGetter for more information + * @see {@link DataProviderGetter} for more information */ DataProviderGetters & Partial>; export default IDataProviderEngine; + +/** + * JSDOC SOURCE DataProviderEngine + * + * Abstract class that provides a placeholder `notifyUpdate` for data provider engine classes. If a + * data provider engine class extends this class, it doesn't have to specify its own `notifyUpdate` + * function in order to use `notifyUpdate`. + * + * @see {@link IDataProviderEngine} for more information on extending this class. + */ +export abstract class DataProviderEngine + implements WithNotifyUpdate +{ + // This is just a placeholder and will be layered over by papi. We don't need it to do anything + // @ts-expect-error ts(6133) `updateInstructions` is not used in this method, but we don't care + // because we want inheriting classes to be able to get this method with Intellisense without + // an underscore that indicates to TypeScript that we aren't using the parameter + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars + notifyUpdate(updateInstructions?: DataProviderUpdateInstructions): void {} +} diff --git a/src/shared/models/data-provider.interface.ts b/src/shared/models/data-provider.interface.ts index f5252f3ca7..878f24e2b6 100644 --- a/src/shared/models/data-provider.interface.ts +++ b/src/shared/models/data-provider.interface.ts @@ -8,8 +8,8 @@ import { Dispose, OnDidDispose } from 'platform-bible-utils'; /** * An object on the papi that manages data and has methods for interacting with that data. Created - * by the papi and layers over an IDataProviderEngine provided by an extension. Returned from - * getting a data provider with dataProviderService.get. + * by the papi and layers over an {@link IDataProviderEngine} provided by an extension. Returned from + * getting a data provider with `papi.dataProviders.get`. * * Note: each `set` method has a corresponding `get` and * `subscribe` method. @@ -30,7 +30,7 @@ export default IDataProvider; * data provider (only the service that set it up should dispose of it) with * dataProviderService.registerEngine * - * @see IDataProvider + * @see {@link IDataProvider} */ // Basically a layer over DisposableNetworkObject // Used to be `DisposableNetworkObject, 'dispose'>>`, , but it had diff --git a/src/shared/models/data-provider.model.ts b/src/shared/models/data-provider.model.ts index 5eeb2f520c..c740f83260 100644 --- a/src/shared/models/data-provider.model.ts +++ b/src/shared/models/data-provider.model.ts @@ -62,7 +62,7 @@ export type DataProviderUpdateInstructions`) * - * @see DataProviderDataTypes for more information + * @see {@link DataProviderDataTypes} for more information */ export type DataTypeNames = keyof TDataTypes & string; @@ -172,7 +172,7 @@ export type DataTypeNames` methods that a data provider provides according to its data types. * - * @see DataProviderSetter for more information + * @see {@link DataProviderSetter} for more information */ export type DataProviderSetters = { [DataType in keyof TDataTypes as `set${DataType & string}`]: DataProviderSetter< @@ -184,7 +184,7 @@ export type DataProviderSetters = { /** * Set of all `get` methods that a data provider provides according to its data types. * - * @see DataProviderGetter for more information + * @see {@link DataProviderGetter} for more information */ export type DataProviderGetters = { [DataType in keyof TDataTypes as `get${DataType & string}`]: DataProviderGetter< @@ -196,7 +196,7 @@ export type DataProviderGetters = { * Set of all `subscribe` methods that a data provider provides according to its data * types. * - * @see DataProviderSubscriber for more information + * @see {@link DataProviderSubscriber} for more information */ export type DataProviderSubscribers = { [DataType in keyof TDataTypes as `subscribe${DataType & string}`]: DataProviderSubscriber< @@ -209,7 +209,7 @@ export type DataProviderSubscribers = * object layers over the data provider engine and runs its methods along with other methods. This * object is transformed into an IDataProvider by networkObjectService.set. * - * @see IDataProvider + * @see {@link IDataProvider} */ type DataProviderInternal = NetworkableObject< diff --git a/src/shared/models/network-object.model.ts b/src/shared/models/network-object.model.ts index 480bc9e101..2831ac0fa2 100644 --- a/src/shared/models/network-object.model.ts +++ b/src/shared/models/network-object.model.ts @@ -18,7 +18,7 @@ import { * call that method. This is because we don't want users of network objects to dispose of them. Only * the caller of `networkObjectService.set` should be able to dispose of the network object. * - * @see networkObjectService + * @see {@link networkObjectService} */ export type NetworkObject = Omit, 'dispose'> & OnDidDispose; @@ -26,14 +26,14 @@ export type NetworkObject = Omit = NetworkObject & Dispose; /** * An object of this type is passed into {@link networkObjectService.set}. * - * @see networkObjectService + * @see {@link networkObjectService} */ export type NetworkableObject = T & CannotHaveOnDidDispose; @@ -56,7 +56,7 @@ export type NetworkableObject = T & CannotHaveOnDidDispose; * Note: This function should return Partial. For some reason, TypeScript can't infer the type * (probably has to do with that it's a wrapped and layered type). Functions that implement this * type should return Partial - * @see networkObjectService + * @see {@link networkObjectService} */ export type LocalObjectToProxyCreator = ( id: string, diff --git a/src/shared/models/project-data-provider-engine.model.ts b/src/shared/models/project-data-provider-engine.model.ts index 570f601590..3054185dd4 100644 --- a/src/shared/models/project-data-provider-engine.model.ts +++ b/src/shared/models/project-data-provider-engine.model.ts @@ -1,9 +1,30 @@ -import { ProjectTypes, ProjectDataTypes } from 'papi-shared-types'; -import type IDataProviderEngine from '@shared/models/data-provider-engine.model'; +import { + ProjectTypes, + ProjectDataTypes, + ProjectStorageInterpreters, + ProjectSettingNames, + ProjectSettingTypes, + ProjectStorageSettingDataScope, + WithProjectDataProviderEngineSettingMethods, +} from 'papi-shared-types'; +import IDataProviderEngine, { DataProviderEngine } from '@shared/models/data-provider-engine.model'; +import { + DataProviderDataType, + DataProviderDataTypes, + DataProviderUpdateInstructions, +} from '@shared/models/data-provider.model'; +import { + ExtensionDataScope, + MandatoryProjectDataTypes, + WithProjectDataProviderEngineExtensionDataMethods, +} from '@shared/models/project-data-provider.model'; +import { ProjectStorageExtensionDataScope } from '@shared/models/project-storage-interpreter.model'; +import { UnsubscriberAsync, UnsubscriberAsyncList } from 'platform-bible-utils'; +import dataProviderService from '@shared/services/data-provider.service'; -/** All possible types for ProjectDataProviderEngines: IDataProviderEngine */ +/** All possible types for ProjectDataProviderEngines: IProjectDataProviderEngine */ export type ProjectDataProviderEngineTypes = { - [ProjectType in ProjectTypes]: IDataProviderEngine; + [ProjectType in ProjectTypes]: IProjectDataProviderEngine; }; export interface ProjectDataProviderEngineFactory { @@ -12,3 +33,230 @@ export interface ProjectDataProviderEngineFactory` and `set + * function` functions. These data types correspond to the `projectType` of the project. + * @see {@link DataProviderDataTypes} for information on how to make powerful types that work well with + * Intellisense. + * + * Note: papi creates a `notifyUpdate` function on the Project Data Provider Engine if one is not + * provided, so it is not necessary to provide one in order to call `this.notifyUpdate`. However, + * TypeScript does not understand that papi will create one as you are writing your Project Data + * Provider Engine, so you can avoid type errors with one of the following options: + * + * 1. If you are using a class to create a Project Data Provider Engine, you can extend the + * {@link ProjectDataProviderEngine} class, and it will provide `notifyUpdate` as well as other helpful + * default method implementations to meet the requirements of {@link MandatoryProjectDataTypes} + * automatically by passing these calls through to the Project Storage Interpreter for you: + * ```typescript + * class MyPDPE extends DataProviderEngine implements IDataProviderEngine { + * ... + * } + * ``` + * + * 2. If you are using an object or class to create a Project Data Provider Engine, you can add a + * `notifyUpdate` function (and, with an object, add the {@link WithNotifyUpdate} type) to + * your Project Data Provider Engine like so: + * ```typescript + * const myPDPE: IProjectDataProviderEngine & WithNotifyUpdate = { + * notifyUpdate(updateInstructions) {}, + * ... + * } + * ``` + * OR + * ```typescript + * class MyPDPE implements IProjectDataProviderEngine { + * notifyUpdate(updateInstructions?: DataProviderEngineNotifyUpdate) {} + * ... + * } + * ``` + */ +export type IProjectDataProviderEngine = + IDataProviderEngine & + WithProjectDataProviderEngineSettingMethods & + WithProjectDataProviderEngineExtensionDataMethods; + +/** + * JSDOC SOURCE ProjectDataProviderEngine + * + * Abstract class that provides default implementations of a number of {@link IProjectDataProvider} + * functions including all the `Setting` and `ExtensionData`-related methods. Extensions can create + * their own Project Data Provider Engine classes and implement this class to meet the requirements + * of {@link MandatoryProjectDataTypes} automatically by passing these calls through to the Project + * Storage Interpreter. This class also subscribes to `Setting` and `ExtensionData` updates from the + * PSI to make sure it keeps its data up-to-date. + * + * This class also provides a placeholder `notifyUpdate` for Project Data Provider Engine classes. + * If a Project Data Provider Engine class extends this class, it doesn't have to specify its own + * `notifyUpdate` function in order to use `notifyUpdate`. + * + * @see {@link IProjectDataProviderEngine} for more information on extending this class. + */ +export abstract class ProjectDataProviderEngine + extends DataProviderEngine< + ProjectDataTypes[ProjectType] & { + // Including `Setting` here so we can emit `Setting` events though the event types are not + // tight enough to use on the actual `Setting` data type and methods + Setting: DataProviderDataType< + ProjectSettingNames, + ProjectSettingTypes[ProjectSettingNames], + ProjectSettingTypes[ProjectSettingNames] + >; + } + > + implements + WithProjectDataProviderEngineSettingMethods, + WithProjectDataProviderEngineExtensionDataMethods +{ + protected readonly projectStorageInterpreter: ProjectStorageInterpreters[ProjectType]; + + private psiSettingUnsubscriberPromise: Promise; + private psiExtensionDataUnsubscriberPromise: Promise; + + /** + * Create a `ProjectDataProviderEngine` instance + * + * @param projectId The project id this Project Data Provider represents for which it serves data + * @param projectStorageInterpreter The {@link IProjectStorageInterpreter} this Project Data + * Provider uses to access raw project data + */ + protected constructor( + protected readonly projectId: string, + protected readonly projectStorageInterpreterId: string, + ) { + super(); + + // TODO: Get the appropriate PSI and pass it in instead of this fake PSI that does literally + // nothing https://github.com/paranext/paranext-core/issues/367 + // @ts-expect-error 2322 + this.projectStorageInterpreter = {}; + + // Set up subscriptions to listen for changes to the PSI Settings and ExtensionData and update + // our own subscribers + this.psiSettingUnsubscriberPromise = this.projectStorageInterpreter.subscribeSetting( + // Just picked a key for no reason in particular because we don't need anything in particular + // here because we're listening for all updates + // TODO: How will we subscribe to all updates from the PSI? We need to notifyUpdate Setting if + // one of our own settings changed, not a setting from literally any project served by the PSI + { key: 'platform.fullName', projectId: this.projectId }, + () => { + this.notifyUpdate('Setting'); + }, + { whichUpdates: '*' }, + ); + this.psiExtensionDataUnsubscriberPromise = + this.projectStorageInterpreter.subscribeExtensionData( + // Just used empty strings because we don't need anything in particular here because we're + // listening for all updates + // TODO: How will we subscribe to all updates from the PSI? We need to notifyUpdate Setting if + // one of our own settings changed, not a setting from literally any project served by the PSI + { dataQualifier: '', extensionName: '', projectId: this.projectId }, + () => { + this.notifyUpdate('ExtensionData'); + }, + { whichUpdates: '*' }, + ); + } + + // Do not emit update events when running this method because we are subscribing to `Setting` + // updates on the PSI and sending out update events in the constructor + @dataProviderService.decorators.doNotNotify + setSetting( + key: ProjectSettingName, + newSetting: ProjectSettingTypes[ProjectSettingName], + ): Promise> { + return this.projectStorageInterpreter.setSetting( + this.getProjectStorageSettingDataScope(key), + newSetting, + ); + } + + // Do not emit update events when running this method because we are subscribing to `Setting` + // updates on the PSI and sending out update events in the constructor + @dataProviderService.decorators.doNotNotify + setExtensionData( + dataScope: ExtensionDataScope, + data: string, + ): Promise> { + return this.projectStorageInterpreter.setExtensionData( + this.getProjectStorageExtensionDataScope(dataScope), + data, + ); + } + + /** + * Get the {@link ProjectStorageSettingDataScope} for this project for the specified project + * setting. This can be used when passing setting-related calls to the Project Storage + * Interpreter. + * + * @param key The string id of the project setting to get a setting data scope for + * @returns `ProjectStorageSettingDataScope` for this project for the specified project setting + */ + @dataProviderService.decorators.ignore + protected getProjectStorageSettingDataScope( + key: ProjectSettingName, + ): ProjectStorageSettingDataScope { + return { projectId: this.projectId, key }; + } + + /** + * Get the {@link ProjectStorageExtensionDataScope} for this project for the specified + * {@link ExtensionDataScope}. This can be used when passing `ExtensionData`-related calls to the + * Project Storage Interpreter. + * + * @param dataScope Information about what data is being referenced by the calling extension given + * to this Project Data Provider + * @returns Information about what data is being referenced by the calling extension that this + * Project Data Provider should give to its Project Storage Interpreter + */ + @dataProviderService.decorators.ignore + protected getProjectStorageExtensionDataScope( + dataScope: ExtensionDataScope, + ): ProjectStorageExtensionDataScope { + return { ...dataScope, projectId: this.projectId }; + } + + getSetting( + key: ProjectSettingName, + ): Promise { + return this.projectStorageInterpreter.getSetting(this.getProjectStorageSettingDataScope(key)); + } + + resetSetting( + key: ProjectSettingName, + ): Promise { + return this.projectStorageInterpreter.resetSetting(this.getProjectStorageSettingDataScope(key)); + } + + getExtensionData(dataScope: ExtensionDataScope): Promise { + return this.projectStorageInterpreter.getExtensionData( + this.getProjectStorageExtensionDataScope(dataScope), + ); + } + + /** + * Disposes of this Project Data Provider Engine. Unsubscribes from listening to the Project + * Storage Interpreter + * + * @returns `true` if successfully unsubscribed + */ + async dispose(): Promise { + const unsubscriberList = new UnsubscriberAsyncList( + `PDP Engine ${this.projectId} PSI Unsubscribers`, + ); + + unsubscriberList.add( + await this.psiSettingUnsubscriberPromise, + await this.psiExtensionDataUnsubscriberPromise, + ); + + return unsubscriberList.runAllUnsubscribers(); + } +} diff --git a/src/shared/models/project-data-provider.model.ts b/src/shared/models/project-data-provider.model.ts index de273195f9..ff5d757288 100644 --- a/src/shared/models/project-data-provider.model.ts +++ b/src/shared/models/project-data-provider.model.ts @@ -1,11 +1,15 @@ -import type { DataProviderDataType } from '@shared/models/data-provider.model'; +import type { + DataProviderDataType, + DataProviderDataTypes, + DataProviderUpdateInstructions, +} from '@shared/models/data-provider.model'; /** Indicates to a PDP what extension data is being referenced */ export type ExtensionDataScope = { /** Name of an extension as provided in its manifest */ extensionName: string; /** - * Name of a unique partition or segment of data within the extension Some examples include (but + * Name of a unique partition or segment of data within the extension. Some examples include (but * are not limited to): * * - Name of an important data structure that is maintained in a project @@ -13,19 +17,54 @@ export type ExtensionDataScope = { * - Name of a resource created by a user that should be maintained in a project * * This is the smallest level of granularity provided by a PDP for accessing extension data. There - * is no way to get or set just a portion of data identified by a single dataQualifier value. + * is no way to get or set just a portion of data identified by a single `dataQualifier` value. */ dataQualifier: string; }; /** + * `DataProviderDataTypes` that each project data provider **must** implement. They are assumed to + * exist and are used by project storage interpreters and other data providers + * + * --- + * + * ### Setting + * + * The `Setting` data type handles getting and setting project settings. All Project Data Providers + * must implement these methods `getSetting` and `setSetting` as well as `resetSetting` in order to + * properly support project settings. In most cases, the Project Data Provider only needs to pass + * the setting calls through to the Project Storage Interpreter. + * + * Note: the `Setting` data type is not actually part of {@link MandatoryProjectDataTypes} because + * the methods would not be able to create a generic type extending from `ProjectSettingNames` in + * order to return the specific setting type being requested. As such, `getSetting`, `setSetting`, + * and `subscribeSetting` are all specified on {@link IProjectDataProvider} instead. Unfortunately, + * as a result, using Intellisense with `useProjectData` will not show `Setting` as a data type + * option, but you can use `useProjectSetting` instead. However, do note that the `Setting` data + * type is fully functional. + * + * The closest possible representation of the `Setting` data type follows: + * + * ```typescript + * Setting: DataProviderDataType< + * ProjectSettingNames, + * ProjectSettingTypes[ProjectSettingNames], + * ProjectSettingTypes[ProjectSettingNames] + * >; + * ``` + * + * --- + * + * ### ExtensionData + * * All Project Data Provider data types must have an `ExtensionData` type. We strongly recommend all * Project Data Provider data types extend from this type in order to standardize the * `ExtensionData` types. * * Benefits of following this standard: * - * - All PSIs that support this `projectType` can use a standardized `ExtensionData` interface + * - All project storage interpreters that support this `projectType` can use a standardized + * `ExtensionData` interface * - If an extension uses the `ExtensionData` endpoint for any project, it will likely use this * standardized interface, so using this interface on your Project Data Provider data types * enables your PDP to support generic extension data @@ -33,6 +72,41 @@ export type ExtensionDataScope = { * so following this interface ensures your PDP will not break if such a requirement is * implemented. */ -export type MandatoryProjectDataType = { +export type MandatoryProjectDataTypes = { ExtensionData: DataProviderDataType; }; + +/** + * The `ExtensionData` methods required for a Project Data Provider Engine to fulfill the + * requirements of {@link MandatoryProjectDataTypes}'s `ExtensionData` data type. + * + * Note: These methods are already covered by {@link MandatoryProjectDataTypes}, but this type adds + * JSDocs for them. + */ +export type WithProjectDataProviderEngineExtensionDataMethods< + TProjectDataTypes extends DataProviderDataTypes, +> = { + /** + * Gets an extension's project data identified by `dataScope` in this project + * + * @param dataScope Information about what data is being referenced by the calling extension given + * to this Project Data Provider + * @returns Extension project data in this project for an extension to use in serving its + * extension project data + */ + getExtensionData(dataScope: ExtensionDataScope): Promise; + /** + * Sets an extension's project data identified by `dataScope` in this project + * + * @param dataScope Information about what data is being referenced by the calling extension given + * to this Project Data Provider + * @param data Updated value of extension project data in this project to set + * @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 + */ + setExtensionData( + dataScope: ExtensionDataScope, + data: string, + ): Promise>; +}; diff --git a/src/shared/models/project-storage-interpreter.model.ts b/src/shared/models/project-storage-interpreter.model.ts new file mode 100644 index 0000000000..4952be1589 --- /dev/null +++ b/src/shared/models/project-storage-interpreter.model.ts @@ -0,0 +1,129 @@ +import { DataProviderDataType } from '@shared/models/data-provider.model'; +import { ExtensionDataScope } from '@shared/models/project-data-provider.model'; + +/** Indicates to a PSI what raw project data chunk is being referenced */ +export type ProjectStorageProjectDataScope = { + /** ID for the project whose raw data chunk to get */ + projectId: string; + /** + * Name of a unique partition or segment of data within the project. Some examples include (but + * are not limited to): + * + * - Name of an important data structure that is maintained in a project + * - Name of a downloaded data set that is being cached + * - Name of a resource created by a user that should be maintained in a project + * + * This is the smallest level of granularity provided by a PSI for accessing raw project data. + * There is no way to get or set just a portion of data identified by a single `dataQualifier` + * value. + */ + dataQualifier: string; +}; + +/** + * `DataProviderDataTypes` that are a sensible default for project storage interpreters to + * implement. Using {@link IProjectStorageInterpreter} without specifying data types will default to + * these data types. These types are simply a recommendation for how to write a PSI for a specified + * `projectType`. As long as both the Project Data Provider and the Project Storage Interpreter for + * a given `projectType` communicate with the same interface, you are free to design the + * communication in the way that makes most sense for the `projectType`. + * + * Note: Project Data Providers are associated to Project Storage Interpreters based on a shared + * `projectType`. A PSI must implement the data types specified for each `projectType` it supports. + * + * --- + * + * ### ProjectData + * + * A simple data type for a Project Data Provider to use to retrieve raw data chunks for a specific + * project from a Project Storage Interpreter with the same `projectType`. The Project Data Provider + * indicates which project it is associated with and specifies the name of a segment of data within + * the project. + * + * Benefits of following this recommendation: + * + * - Serving raw data chunks according to a simple specifier keeps the Project Storage Interpreter + * thin and simple so multiple thin PSIs can be made for different `storageType`s while leaving + * the complex task of parsing and serving project data to the Project Data Provider. + * - This is an easy pattern to follow when starting to learn how to make new `projectType`s in + * Platform.Bible. + */ +export type DefaultProjectStorageDataTypes = { + ProjectData: DataProviderDataType; +}; + +/** + * Indicates to a PSI what extension data is being referenced on what project. Generally, a PDP + * passes calls to `ExtensionData` data type methods to its PSI and adds the `projectId`. + */ +export type ProjectStorageExtensionDataScope = ExtensionDataScope & { + /** ID for the project whose extension data to get */ + projectId: string; +}; + +/** + * `DataProviderDataTypes` that each project storage interpreter must implement. They are assumed to + * exist and are used by project data providers + * + * --- + * + * ### Setting + * + * The `Setting` data type handles getting and setting project settings. All Project Storage + * Interpreters must implement these methods `getSetting` and `setSetting` as well as `resetSetting` + * in order to properly support project settings. In most cases, the Project Data Provider will pass + * `Setting` calls through to the Project Storage Interpreter. + * + * Note: the `Setting` data type is not actually part of {@link MandatoryProjectStorageDataTypes} + * because the methods would not be able to create a generic type extending from + * `ProjectSettingNames` in order to return the specific setting type being requested. As such, + * `getSetting`, `setSetting`, and `subscribeSetting` are all specified on + * {@link IProjectStorageInterpreter} instead. However, do note that the `Setting` data type is fully + * functional. + * + * The closest possible representation of the `Setting` data type follows: + * + * ```typescript + * Setting: DataProviderDataType< + * ProjectStorageSettingDataScope, + * ProjectSettingTypes[ProjectSettingNames], + * ProjectSettingTypes[ProjectSettingNames] + * >; + * ``` + * + * WARNING: Each Project Storage Interpreter **needs** to fulfill the following requirements for its + * settings-related methods: + * + * - `getSetting`: if a project setting value is present for the key requested, return it. Otherwise, + * you must call `papi.projectSettings.getDefault` to get the default value or throw if that call + * throws. This functionality preserves the intended type of the setting and avoids returning + * `undefined` unexpectedly. + * - `setSetting`: must call `papi.projectSettings.isValid` before setting the value and should return + * false if the call returns `false`. This functionality preserves the intended intended type of + * the setting and avoids allowing the setting to be set to the wrong type. + * - `resetSetting`: deletes the value at the key and sends a setting update event. After this, + * `getSetting` should see the setting value as not present and return the default value again. + * - Note: see {@link IProjectStorageInterpreter} for method signatures for these three methods. + * + * .--- + * + * ### ExtensionData + * + * All Project Storage Interpreter data types must have an `ExtensionData` type. We strongly + * recommend all Project Storage Interpreter data types extend from this type in order to + * standardize the `ExtensionData` types. Project Data Providers will call this endpoint in order to + * retrieve extensions' project data. + * + * Benefits of following this standard: + * + * - Project data providers of this `projectType` can use a standardized `ExtensionData` interface + * - If an extension uses the `ExtensionData` endpoint for any project, it will likely use this + * standardized interface, so using this interface on your Project Storage Interpreter data types + * enables your PSI to support generic extension data + * - In the future, we may enforce that callers to `ExtensionData` endpoints include `extensionName`, + * so following this interface ensures your PSI will not break if such a requirement is + * implemented. + */ +export type MandatoryProjectStorageDataTypes = { + ExtensionData: DataProviderDataType; +}; diff --git a/src/shared/services/data-provider.service.ts b/src/shared/services/data-provider.service.ts index 617b245703..7bb158c582 100644 --- a/src/shared/services/data-provider.service.ts +++ b/src/shared/services/data-provider.service.ts @@ -11,9 +11,7 @@ import DataProviderInternal, { getDataProviderDataTypeFromFunctionName, DataProviderDataType, } from '@shared/models/data-provider.model'; -import IDataProviderEngine, { - DataProviderEngineNotifyUpdate, -} from '@shared/models/data-provider-engine.model'; +import IDataProviderEngine, { DataProviderEngine } from '@shared/models/data-provider-engine.model'; import { PlatformEvent, PlatformEventEmitter, @@ -66,21 +64,6 @@ let isInitialized = false; /** Promise that resolves when this service is finished initializing */ let initializePromise: Promise | undefined; -/** - * JSDOC SOURCE DataProviderEngine - * - * Abstract class that provides a placeholder `notifyUpdate` for data provider engine classes. If a - * data provider engine class extends this class, it doesn't have to specify its own `notifyUpdate` - * function in order to use `notifyUpdate`. - * - * @see IDataProviderEngine for more information on extending this class. - */ -export abstract class DataProviderEngine { - // This is just a placeholder and will be layered over by papi. We don't need it to do anything - // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars - notifyUpdate: DataProviderEngineNotifyUpdate = (_updateInstructions) => {}; -} - /** Sets up the service. Only runs once and always returns the same promise after that */ const initialize = () => { if (initializePromise) return initializePromise; @@ -98,6 +81,8 @@ const initialize = () => { }; /** + * JSDOC SOURCE DataProviderServiceHasKnown + * * Indicate if we are aware of an existing data provider with the given name. If a data provider * with the given name is somewhere else on the network, this function won't tell you about it * unless something else in the existing process is subscribed to it. @@ -386,6 +371,8 @@ function mapUpdateInstructionsToUpdateEvent(target: T, member: keyof T): void; -function ignore( - target: T | (Function & { isIgnored?: boolean }), - member?: keyof T, -): void { - if (typeof target === 'function') target.isIgnored = true; - else { - // We don't care what type the decorated object is. Just want to set some function metadata - // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion - (target[member as keyof T] as any).isIgnored = true; +function ignore(target: object, member: string): void; +// We don't care what type the decorated object is. Just want to set some function metadata +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function ignore(target: any, member?: string): void { + if (typeof target === 'function') { + target.isIgnored = true; + return; + } + + if (!member) return; + + target[member].isIgnored = true; +} + +/** + * JSDOC SOURCE DataProviderServiceDecoratorsDoNotNotify + * + * Decorator function that marks a data provider engine `set` method not to automatically + * emit an update and notify subscribers of a change to the data. papi will still consider the + * `set` method to be a data type method, but it will not layer over it to emit updates. + * + * @example Use this as a decorator on a class's method: + * + * ```typescript + * class MyDataProviderEngine { + * @papi.dataProviders.decorators.doNotNotify + * async setVerse() {} + * } + * ``` + * + * WARNING: Do not copy and paste this example. The `@` symbol does not render correctly in JSDoc + * code blocks, so a different unicode character was used. Please use a normal `@` when using a + * decorator. + * + * OR + * + * @example Call this function signature on an object's method: + * + * ```typescript + * const myDataProviderEngine = { + * async setVerse() {}, + * }; + * papi.dataProviders.decorators.ignore(dataProviderEngine.setVerse); + * ``` + * + * @param method The method not to layer over to send an automatic update + */ +function doNotNotify(method: Function & { doNotNotify?: boolean }): void; +/** + * Decorator function that marks a data provider engine `set` method not to automatically + * emit an update and notify subscribers of a change to the data. papi will still consider the + * `set` method to be a data type method, but it will not layer over it to emit updates. + * + * @param target The class that has the method not to layer over to send an automatic update + * @param member The name of the method not to layer over to send an automatic update + * + * Note: this is the signature that provides the actual decorator functionality. However, since + * users will not be using this signature, the example usage is provided in the signature above. + */ +function doNotNotify(target: object, member: string): void; +// We don't care what type the decorated object is. Just want to set some function metadata +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function doNotNotify(target: any, member?: string): void { + if (typeof target === 'function') { + target.doNotNotify = true; + return; } + + if (!member) return; + + target[member].doNotNotify = true; } /** + * JSDOC SOURCE DataProviderServiceDecorators + * * A collection of decorators to be used with the data provider service * * @example To use the `ignore` a decorator on a class's method: @@ -456,7 +505,10 @@ function ignore( * decorator. */ const decorators = { + /** JSDOC DESTINATION DataProviderServiceDecoratorsIgnore */ ignore, + /** JSDOC DESTINATION DataProviderServiceDecoratorsDoNotNotify */ + doNotNotify, }; /** @@ -500,8 +552,6 @@ function buildDataProvider( [...getAllObjectFunctionNames(dataProviderEngine)], (fnName) => { // If the function was decorated with @ignore, do not consider it a special function - // We don't care about types. We just want to check the decorator - // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion if (dataProviderEngineUntyped[fnName].isIgnored) return 'other'; if (fnName.startsWith('get')) return 'get'; @@ -545,7 +595,13 @@ function buildDataProvider( // Layer over the data provider engine's set methods with set methods that actually emit an update // if they return true dataTypes.get('set')?.forEach((dataType) => { - if (dataProviderEngineUntyped[`set${dataType}`]) { + // If the function was decorated with @doNotNotify, do not overwrite it to automatically emit an update + if ( + dataProviderEngineUntyped[`set${dataType}`] && + // We don't care about types. We just want to check the decorator + // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion + !(dataProviderEngineUntyped[`set${dataType}`] as any).doNotNotify + ) { /* eslint-disable no-type-assertion/no-type-assertion */ /** Saved bound version of the data provider engine's set so we can call it from here */ const dpeSet = ( @@ -579,6 +635,8 @@ function buildDataProvider( } /** + * JSDOC SOURCE DataProviderServiceRegisterEngine + * * Creates a data provider to be shared on the network layering over the provided data provider * engine. * @@ -728,6 +786,8 @@ function createLocalDataProviderToProxy>( // Declare an interface for the object we're exporting so that JSDoc comments propagate export interface DataProviderService { + /** JSDOC DESTINATION DataProviderServiceHasKnown */ hasKnown: typeof hasKnown; + /** JSDOC DESTINATION DataProviderServiceRegisterEngine */ registerEngine: typeof registerEngine; + /** JSDOC DESTINATION DataProviderServiceGet */ get: typeof get; + /** JSDOC DESTINATION DataProviderServiceDecorators */ decorators: typeof decorators; + /** JSDOC DESTINATION DataProviderEngine */ DataProviderEngine: typeof DataProviderEngine; } diff --git a/src/shared/services/papi-core.service.ts b/src/shared/services/papi-core.service.ts index 9307b645ce..edc035a2ab 100644 --- a/src/shared/services/papi-core.service.ts +++ b/src/shared/services/papi-core.service.ts @@ -20,9 +20,10 @@ export type { default as IDataProviderEngine } from '@shared/models/data-provide export type { DialogOptions } from '@shared/models/dialog-options.model'; export type { ExtensionDataScope, - MandatoryProjectDataType, + MandatoryProjectDataTypes, } from '@shared/models/project-data-provider.model'; export type { ProjectMetadata } from '@shared/models/project-metadata.model'; +export type { MandatoryProjectStorageDataTypes } from '@shared/models/project-storage-interpreter.model'; export type { GetWebViewOptions, SavedWebViewDefinition, @@ -33,3 +34,5 @@ export type { } from '@shared/models/web-view.model'; export type { IWebViewProvider } from '@shared/models/web-view-provider.model'; + +export type { SimultaneousProjectSettingsChanges } from '@shared/services/project-settings.service-model'; diff --git a/src/shared/services/project-data-provider.service.ts b/src/shared/services/project-data-provider.service.ts index 25298ee5e9..734f89d3bd 100644 --- a/src/shared/services/project-data-provider.service.ts +++ b/src/shared/services/project-data-provider.service.ts @@ -10,6 +10,10 @@ import { Dispose, UnsubscriberAsyncList } from 'platform-bible-utils'; import projectLookupService from '@shared/services/project-lookup.service'; import logger from '@shared/services/logger.service'; +/** + * Class that creates Project Data Providers of a specified `projectType`. Layers over + * extension-provided {@link ProjectDataProviderEngineFactory}. Internal only + */ class ProjectDataProviderFactory { private readonly pdpIds: Map = new Map(); private readonly projectType: ProjectType; @@ -59,8 +63,12 @@ class ProjectDataProviderFactory { projectId: string, projectStorageInterpreterId: string, ): Promise { - // Add a check for extensions that provide new project types - if (!('getExtensionData' in projectDataProviderEngine)) + // Add a check for extensions that provide new project types to make sure they fulfill + // `MandatoryProjectDataTypes` + if ( + !('getExtensionData' in projectDataProviderEngine) || + !('getSetting' in projectDataProviderEngine) + ) throw new Error('projectDataProviderEngine must implement "MandatoryProjectDataTypes"'); const pdpId: string = `${newNonce()}-pdp`; const pdp = await registerEngineByType( diff --git a/src/shared/services/project-settings.service-model.ts b/src/shared/services/project-settings.service-model.ts new file mode 100644 index 0000000000..14ee71c864 --- /dev/null +++ b/src/shared/services/project-settings.service-model.ts @@ -0,0 +1,65 @@ +import { ProjectSettingNames, ProjectSettingTypes, ProjectTypes } from 'papi-shared-types'; + +/** + * JSDOC SOURCE projectSettingsService + * + * Provides utility functions that project storage interpreters should call when handling project + * settings + */ +export interface IProjectSettingsService { + /** + * Calls registered project settings validators to determine whether or not a project setting + * change is valid. + * + * Every Project Storage Interpreter **must** run this function when it receives a request to set + * a project setting before changing the value of the setting. + * + * @param newValue The new value requested to set the project setting value to + * @param currentValue The current project setting value + * @param key The project setting key being set + * @param allChanges All project settings changes being set in one batch + * @param projectType The `projectType` for the project whose setting is being changed + * @returns `true` if change is valid, `false` otherwise + */ + isValid( + newValue: ProjectSettingTypes[ProjectSettingName], + currentValue: ProjectSettingTypes[ProjectSettingName], + key: ProjectSettingName, + allChanges: SimultaneousProjectSettingsChanges, + projectType: ProjectTypes, + ): Promise; + /** + * Gets default value for a project setting + * + * Every Project Storage Interpreter **must** run this function when it receives a request to get + * a project setting if the project does not have a value for the project setting requested. It + * should return the response from this function directly, either the returned default value or + * throw. + * + * @param key The project setting key for which to get the default value + * @param projectType The `projectType` to get default setting value for + * @returns The default value for the setting if a default value is registered + * @throws If a default value is not registered for the setting + */ + getDefault( + key: ProjectSettingName, + projectType: ProjectTypes, + ): Promise; +} + +/** + * All project settings changes being set in one batch + * + * Project settings may be circularly dependent on one another, so multiple project settings may + * need to be changed at once in some cases + */ +export type SimultaneousProjectSettingsChanges = { + [ProjectSettingName in ProjectSettingNames]?: { + /** The new value requested to set the project setting value to */ + newValue: ProjectSettingTypes[ProjectSettingName]; + /** The current project setting value */ + currentValue: ProjectSettingTypes[ProjectSettingName]; + }; +}; + +export const projectSettingsServiceNetworkObjectName = 'ProjectSettingsService'; diff --git a/src/shared/services/project-settings.service.ts b/src/shared/services/project-settings.service.ts new file mode 100644 index 0000000000..5040d78a06 --- /dev/null +++ b/src/shared/services/project-settings.service.ts @@ -0,0 +1,45 @@ +import { + projectSettingsServiceNetworkObjectName, + IProjectSettingsService, +} from '@shared/services/project-settings.service-model'; +import networkObjectService from '@shared/services/network-object.service'; + +let networkObject: IProjectSettingsService; +let initializationPromise: Promise; +async function initialize(): Promise { + if (!initializationPromise) { + initializationPromise = new Promise((resolve, reject) => { + const executor = async () => { + try { + const localProjectSettingsService = + await networkObjectService.get( + projectSettingsServiceNetworkObjectName, + ); + if (!localProjectSettingsService) + throw new Error( + `${projectSettingsServiceNetworkObjectName} is not available as a network object`, + ); + networkObject = localProjectSettingsService; + resolve(); + } catch (error) { + reject(error); + } + }; + executor(); + }); + } + return initializationPromise; +} + +const projectSettingsService: IProjectSettingsService = { + async getDefault(key, projectType) { + await initialize(); + return networkObject.getDefault(key, projectType); + }, + async isValid(newValue, currentValue, key, allChanges, projectType) { + await initialize(); + return networkObject.isValid(newValue, currentValue, key, allChanges, projectType); + }, +}; + +export default projectSettingsService; diff --git a/src/shared/services/settings.service-model.ts b/src/shared/services/settings.service-model.ts index d1decff4dc..0561264a89 100644 --- a/src/shared/services/settings.service-model.ts +++ b/src/shared/services/settings.service-model.ts @@ -27,10 +27,14 @@ export const settingsServiceObjectToProxy = Object.freeze({ * 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. + * + * The closest possible representation of the unnamed (````) data type follows: + * + * ```typescript + * '': DataProviderDataType; + * ``` */ -export type SettingDataTypes = { - // '': DataProviderDataType; -}; +export type SettingDataTypes = {}; export type AllSettingsData = { [SettingName in SettingNames]: SettingTypes[SettingName];