From 00d132b70081ee4f0f8347c3ae04bac37ec4e167 Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Mon, 5 Feb 2024 11:02:23 -0500 Subject: [PATCH 01/19] initial commit --- src/declarations/papi-shared-types.ts | 1 + src/main/services/settings.service-host.ts | 20 +++++++++++++++++++ src/shared/services/settings.service-model.ts | 6 ++++++ 3 files changed, 27 insertions(+) create mode 100644 src/main/services/settings.service-host.ts create mode 100644 src/shared/services/settings.service-model.ts diff --git a/src/declarations/papi-shared-types.ts b/src/declarations/papi-shared-types.ts index dab6c075e9..47d59bf969 100644 --- a/src/declarations/papi-shared-types.ts +++ b/src/declarations/papi-shared-types.ts @@ -55,6 +55,7 @@ declare module 'papi-shared-types' { export interface SettingTypes { 'platform.verseRef': ScriptureReference; + // 'platform.interfaceLanguage': InterfaceLanguage; // With only one key in this interface, `papi.d.ts` was baking in the literal string when // `SettingNames` was being used. Adding a placeholder key makes TypeScript generate `papi.d.ts` // correctly. When we add another setting, we can remove this placeholder. diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts new file mode 100644 index 0000000000..b07609bcdb --- /dev/null +++ b/src/main/services/settings.service-host.ts @@ -0,0 +1,20 @@ +import IDataProviderEngine from '@shared/models/data-provider-engine.model'; +import { DataProviderEngine } from '@shared/services/data-provider.service'; +import { SettingDataTypes } from '@shared/services/settings.service-model'; +import { SettingNames } from 'papi-shared-types'; + +class SettingDataProviderEngine + extends DataProviderEngine + implements IDataProviderEngine +{ + // Where do our settings live? Some JSON object/file/whatever? + + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor() { + super(); + } + + async getSetting(key: string): Promise {} + + async setSetting(key: string, value: SettingNames): Promise {} +} diff --git a/src/shared/services/settings.service-model.ts b/src/shared/services/settings.service-model.ts new file mode 100644 index 0000000000..b1dd9854e2 --- /dev/null +++ b/src/shared/services/settings.service-model.ts @@ -0,0 +1,6 @@ +import { SettingNames } from 'papi-shared-types'; +import { DataProviderDataType } from './papi-core.service'; + +export type SettingDataTypes = { + setting: DataProviderDataType; // ?? +}; From 8ed04f33bb12807c00afc3c6444bb13efeed0c9b Mon Sep 17 00:00:00 2001 From: Jolie Rabideau Date: Mon, 5 Feb 2024 12:22:15 -0500 Subject: [PATCH 02/19] format service, build model --- src/main/services/settings.service-host.ts | 111 ++++++++++- src/shared/services/papi-core.service.ts | 3 +- src/shared/services/settings.service-model.ts | 106 ++++++++++- src/shared/services/settings.service.ts | 178 +++++------------- 4 files changed, 260 insertions(+), 138 deletions(-) diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index b07609bcdb..faa653c6a4 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -1,11 +1,15 @@ import IDataProviderEngine from '@shared/models/data-provider-engine.model'; -import { DataProviderEngine } from '@shared/services/data-provider.service'; -import { SettingDataTypes } from '@shared/services/settings.service-model'; -import { SettingNames } from 'papi-shared-types'; +import dataProviderService, { DataProviderEngine } from '@shared/services/data-provider.service'; +import { ISettingsService, ResetSettingEvent, SettingDataTypes, SettingEvent, UpdateSettingEvent, onDidUpdateSettingEmitters, settingsServiceDataProviderName, settingsServiceObjectToProxy } from '@shared/services/settings.service-model'; +import { SettingNames, SettingTypes } from 'papi-shared-types'; +import { PlatformEventEmitter, UnsubscriberAsync, serialize } from 'platform-bible-utils'; +// TODO: 3 Fix function declarations to match service-model +// TODO: 4 Fix implementation of all functions +// TODO: Where is data stored, how is dp going to access it class SettingDataProviderEngine - extends DataProviderEngine - implements IDataProviderEngine + extends DataProviderEngine> + implements IDataProviderEngine> { // Where do our settings live? Some JSON object/file/whatever? @@ -14,7 +18,100 @@ class SettingDataProviderEngine super(); } - async getSetting(key: string): Promise {} + async getSetting( + key: SettingName, + defaultSetting: SettingTypes[SettingName], + ): Promise => { + const settingString = localStorage.getItem(key); + // Null is used by the external API + // eslint-disable-next-line no-null/no-null + if (settingString !== null) { + return deserialize(settingString); + } + return defaultSetting; + } + + async setSetting( + key: SettingName, + newSetting: SettingTypes[SettingName], + ) => { + localStorage.setItem(key, serialize(newSetting)); + // Assert type of the particular SettingName of the emitter. + // eslint-disable-next-line no-type-assertion/no-type-assertion + const emitter = onDidUpdateSettingEmitters.get(key); + const setMessage: UpdateSettingEvent = { + setting: newSetting, + type: 'update-setting', + }; + emitter?.emit(setMessage); + } - async setSetting(key: string, value: SettingNames): Promise {} + async resetSetting(key: SettingName) => { + localStorage.removeItem(key); + // Assert type of the particular SettingName of the emitter. + // eslint-disable-next-line no-type-assertion/no-type-assertion + const emitter = onDidUpdateSettingEmitters.get(key); + const resetMessage: ResetSettingEvent = { type: 'reset-setting' }; + emitter?.emit(resetMessage); + } + + async subscribeToSetting( + key: SettingName, + callback: (newSetting: SettingEvent) => void, + ): Promise => { + // Assert type of the particular SettingName of the emitter. + // eslint-disable-next-line no-type-assertion/no-type-assertion + let emitter = onDidUpdateSettingEmitters.get(key) as + | PlatformEventEmitter> + | undefined; + if (!emitter) { + emitter = new PlatformEventEmitter>(); + onDidUpdateSettingEmitters.set( + key, + // Assert type of the general SettingNames of the emitter. + // eslint-disable-next-line no-type-assertion/no-type-assertion + emitter as PlatformEventEmitter>, + ); + } + return emitter.subscribe(callback); + } } + +let initializationPromise: Promise; +/** Need to run initialize before using this */ +let dataProvider: ISettingsService; +export async function initialize(): Promise { + if (!initializationPromise) { + initializationPromise = new Promise((resolve, reject) => { + const executor = async () => { + try { + dataProvider = await dataProviderService.registerEngine( + settingsServiceDataProviderName, + new SettingDataProviderEngine(), // will be fixed when dp types are correct + ); + resolve(); + } catch (error) { + reject(error); + } + }; + executor(); + }); + } + return initializationPromise; +} + +/** This is an internal-only export for testing purposes, and should not be used in development */ +export const testingSettingService = { + implementSettingDataProviderEngine: () => { + return new SettingDataProviderEngine(); + }, +}; + +// This will be needed later for disposing of the data provider, choosing to ignore instead of +// remove code that will be used later +// @ts-ignore 6133 +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const settingService = createSyncProxyForAsyncObject(async () => { + await initialize(); + return dataProvider; +}, settingsServiceObjectToProxy); diff --git a/src/shared/services/papi-core.service.ts b/src/shared/services/papi-core.service.ts index 7d39fb3df5..9d68fc61a9 100644 --- a/src/shared/services/papi-core.service.ts +++ b/src/shared/services/papi-core.service.ts @@ -34,4 +34,5 @@ export type { export type { IWebViewProvider } from '@shared/models/web-view-provider.model'; -export type { SettingEvent } from '@shared/services/settings.service'; +// TODO: Do we need this here? How to fix? +// export type { SettingEvent } from '@shared/services/settings.service-model'; diff --git a/src/shared/services/settings.service-model.ts b/src/shared/services/settings.service-model.ts index b1dd9854e2..54fdd48a69 100644 --- a/src/shared/services/settings.service-model.ts +++ b/src/shared/services/settings.service-model.ts @@ -1,6 +1,104 @@ -import { SettingNames } from 'papi-shared-types'; -import { DataProviderDataType } from './papi-core.service'; +import { SettingNames, SettingTypes } from 'papi-shared-types'; +import { OnDidDispose, PlatformEventEmitter, UnsubscriberAsync } from 'platform-bible-utils'; +import { + DataProviderDataType, + DataProviderUpdateInstructions, + IDataProvider, +} from './papi-core.service'; -export type SettingDataTypes = { - setting: DataProviderDataType; // ?? +/** + * Name used to register the data provider + * + * You can use this name + */ +export const settingsServiceDataProviderName = 'platform.settingsServiceDataProvider'; +export const settingsServiceObjectToProxy = Object.freeze({ + dataProviderName: settingsServiceDataProviderName, +}); + +// TODO: 1 Fix types- should they be like string/T or SettingName extends SettingNames +export type SettingDataTypes = { + Setting: DataProviderDataType; +}; + +declare module 'papi-shared-types' { + export interface DataProviders { + [settingsServiceDataProviderName]: ISettingsService; + } +} + +/** Event to set or update a setting */ +export type UpdateSettingEvent = { + type: 'update-setting'; + setting: SettingTypes[SettingName]; +}; + +/** Event to remove a setting */ +export type ResetSettingEvent = { + type: 'reset-setting'; }; + +/** All supported setting events */ +export type SettingEvent = + | UpdateSettingEvent + | ResetSettingEvent; + +/** All message subscriptions - emitters that emit an event each time a setting is updated */ +export const onDidUpdateSettingEmitters = new Map< + SettingNames, + PlatformEventEmitter> +>(); + +// TODO: 2 Fix function declarations to match data provider data types +/** JSDOC SOURCE settingsService */ +export type ISettingsService = { + /** + * Retrieves the value of the specified setting + * + * @param key The string id of the setting for which the value is being retrieved + * @param defaultSetting The default value used for the setting if no value is available for the + * key + * @returns The value of the specified setting, parsed to an object. Returns default setting if + * setting does not exist + */ + getSetting( + key: SettingName, + defaultSetting: SettingTypes[SettingName], + ): Promise; + + /** + * Sets the value of the specified setting + * + * @param key The string id of the setting for which the value is being retrieved + * @param newSetting The value that is to be stored. Setting the new value to `undefined` is the + * equivalent of deleting the setting + */ + setSetting( + key: SettingName, + newSetting: SettingTypes[SettingName], + ): Promise>>; + + /** + * Removes the setting from memory + * + * @param key The string id of the setting for which the value is being removed + */ + resetSetting( + key: SettingName, + ): Promise>>; + + /** + * Subscribes to updates of the specified setting. Whenever the value of the setting changes, the + * callback function is executed. + * + * @param key The string id of the setting for which the value is being subscribed to + * @param callback The function that will be called whenever the specified setting is updated + * @returns Unsubscriber that should be called whenever the subscription should be deleted + */ + subscribeSetting( + key: SettingName, + callback: (newSetting: SettingEvent) => void, + ): Promise; +} & OnDidDispose & + IDataProvider> & + typeof settingsServiceObjectToProxy; diff --git a/src/shared/services/settings.service.ts b/src/shared/services/settings.service.ts index 586c6c85eb..c661f3e09a 100644 --- a/src/shared/services/settings.service.ts +++ b/src/shared/services/settings.service.ts @@ -1,131 +1,57 @@ -import { Unsubscriber, deserialize, serialize, PlatformEventEmitter } from 'platform-bible-utils'; -import { SettingNames, SettingTypes } from 'papi-shared-types'; - -/** Event to set or update a setting */ -export type UpdateSettingEvent = { - type: 'update-setting'; - setting: SettingTypes[SettingName]; -}; - -/** Event to remove a setting */ -export type ResetSettingEvent = { - type: 'reset-setting'; -}; - -/** All supported setting events */ -export type SettingEvent = - | UpdateSettingEvent - | ResetSettingEvent; - -/** All message subscriptions - emitters that emit an event each time a setting is updated */ -const onDidUpdateSettingEmitters = new Map< - SettingNames, - PlatformEventEmitter> ->(); - -/** - * Retrieves the value of the specified setting - * - * @param key The string id of the setting for which the value is being retrieved - * @param defaultSetting The default value used for the setting if no value is available for the key - * @returns The value of the specified setting, parsed to an object. Returns default setting if - * setting does not exist - */ -const getSetting = ( - key: SettingName, - defaultSetting: SettingTypes[SettingName], -): SettingTypes[SettingName] => { - const settingString = localStorage.getItem(key); - // Null is used by the external API - // eslint-disable-next-line no-null/no-null - if (settingString !== null) { - return deserialize(settingString); +import dataProviderService from './data-provider.service'; +import { + ISettingsService, + settingsServiceDataProviderName, + settingsServiceObjectToProxy, +} from './settings.service-model'; + +let dataProvider: ISettingsService; +let initializationPromise: Promise; +async function initialize(): Promise { + if (!initializationPromise) { + initializationPromise = new Promise((resolve, reject) => { + const executor = async () => { + try { + const provider = await dataProviderService.get(settingsServiceDataProviderName); + if (!provider) throw new Error('Menu data service undefined'); + dataProvider = provider; + resolve(); + } catch (error) { + reject(error); + } + }; + executor(); + }); } - return defaultSetting; -}; - -/** - * Sets the value of the specified setting - * - * @param key The string id of the setting for which the value is being retrieved - * @param newSetting The value that is to be stored. Setting the new value to `undefined` is the - * equivalent of deleting the setting - */ -const setSetting = ( - key: SettingName, - newSetting: SettingTypes[SettingName], -) => { - localStorage.setItem(key, serialize(newSetting)); - // Assert type of the particular SettingName of the emitter. - // eslint-disable-next-line no-type-assertion/no-type-assertion - const emitter = onDidUpdateSettingEmitters.get(key); - const setMessage: UpdateSettingEvent = { - setting: newSetting, - type: 'update-setting', - }; - emitter?.emit(setMessage); -}; - -/** - * Removes the setting from memory - * - * @param key The string id of the setting for which the value is being removed - */ -const resetSetting = (key: SettingName) => { - localStorage.removeItem(key); - // Assert type of the particular SettingName of the emitter. - // eslint-disable-next-line no-type-assertion/no-type-assertion - const emitter = onDidUpdateSettingEmitters.get(key); - const resetMessage: ResetSettingEvent = { type: 'reset-setting' }; - emitter?.emit(resetMessage); -}; + return initializationPromise; +} -/** - * Subscribes to updates of the specified setting. Whenever the value of the setting changes, the - * callback function is executed. - * - * @param key The string id of the setting for which the value is being subscribed to - * @param callback The function that will be called whenever the specified setting is updated - * @returns Unsubscriber that should be called whenever the subscription should be deleted - */ -const subscribeToSetting = ( - key: SettingName, - callback: (newSetting: SettingEvent) => void, -): Unsubscriber => { - // Assert type of the particular SettingName of the emitter. +// TODO: delete this utility function once menuDataService is pushed- don't have access to it now +function createSyncProxyForAsyncObject( + getObject: (args?: unknown[]) => Promise, + objectToProxy: Partial = {}, +): T { + // objectToProxy will have only the synchronously accessed properties of T on it, and this proxy + // makes the async methods that do not exist yet available synchronously so we have all of T // eslint-disable-next-line no-type-assertion/no-type-assertion - let emitter = onDidUpdateSettingEmitters.get(key) as - | PlatformEventEmitter> - | undefined; - if (!emitter) { - emitter = new PlatformEventEmitter>(); - onDidUpdateSettingEmitters.set( - key, - // Assert type of the general SettingNames of the emitter. - // eslint-disable-next-line no-type-assertion/no-type-assertion - emitter as PlatformEventEmitter>, - ); - } - return emitter.subscribe(callback); -}; - -// Declare an interface for the object we're exporting so that JSDoc comments propagate -export interface SettingsService { - get: typeof getSetting; - set: typeof setSetting; - reset: typeof resetSetting; - subscribe: typeof subscribeToSetting; + return new Proxy(objectToProxy as T, { + get(target, prop) { + // We don't have any type information for T, so we assume methodName exists on it and will let JavaScript throw if it doesn't exist + // @ts-expect-error 7053 + if (prop in target) return target[prop]; + return async (...args: unknown[]) => { + // 7053: We don't have any type information for T, so we assume methodName exists on it and will let JavaScript throw if it doesn't exist + // 2556: The args here are the parameters for the method specified + // @ts-expect-error 7053 2556 + return (await getObject())[prop](...args); + }; + }, + }); } -/** - * JSDOC SOURCE settingsService - * - * Service that allows to get and set settings in local storage - */ -const settingsService: SettingsService = { - get: getSetting, - set: setSetting, - reset: resetSetting, - subscribe: subscribeToSetting, -}; -export default settingsService; +const menuDataService = createSyncProxyForAsyncObject(async () => { + await initialize(); + return dataProvider; +}, settingsServiceObjectToProxy); + +export default menuDataService; From abebb9a60d9039c49a936c8ecea5d93e4e2b1e06 Mon Sep 17 00:00:00 2001 From: Jolie Rabideau Date: Tue, 6 Feb 2024 12:06:17 -0500 Subject: [PATCH 03/19] data provider types, adding settings info data --- src/main/data/core-settings-info.data.ts | 18 +++++ src/main/services/settings.service-host.ts | 68 +++++++++++++++---- src/shared/services/settings.service-model.ts | 63 +++++++++++++---- src/shared/services/settings.service.ts | 24 +------ 4 files changed, 121 insertions(+), 52 deletions(-) create mode 100644 src/main/data/core-settings-info.data.ts diff --git a/src/main/data/core-settings-info.data.ts b/src/main/data/core-settings-info.data.ts new file mode 100644 index 0000000000..82b23dcd18 --- /dev/null +++ b/src/main/data/core-settings-info.data.ts @@ -0,0 +1,18 @@ +import { SettingNames, SettingTypes } from 'papi-shared-types'; + +/** Information about one setting */ +type SettingInfo = { + default: SettingTypes[SettingName]; +}; + +/** Information about all settings. Keys are setting keys, values are information for that setting */ +export type AllSettingsInfo = { + [SettingName in SettingNames]: SettingInfo; +}; + +/** Info about all settings built into core. Does not contain info for extensions' settings */ +const coreSettingsInfo: Partial = { + 'platform.verseRef': { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } }, +}; + +export default coreSettingsInfo; diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index faa653c6a4..18a501675e 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -1,40 +1,64 @@ import IDataProviderEngine from '@shared/models/data-provider-engine.model'; +import { DataProviderUpdateInstructions } from '@shared/models/data-provider.model'; import dataProviderService, { DataProviderEngine } from '@shared/services/data-provider.service'; -import { ISettingsService, ResetSettingEvent, SettingDataTypes, SettingEvent, UpdateSettingEvent, onDidUpdateSettingEmitters, settingsServiceDataProviderName, settingsServiceObjectToProxy } from '@shared/services/settings.service-model'; +import { + createSyncProxyForAsyncObject, + ISettingsService, + ResetSettingEvent, + SettingDataTypes, + SettingEvent, + UpdateSettingEvent, + onDidUpdateSettingEmitters, + settingsServiceDataProviderName, + settingsServiceObjectToProxy, + SettingsValues, +} from '@shared/services/settings.service-model'; +import * as nodeFS from '@node/services/node-file-system.service'; +import coreSettingsInfo, { AllSettingsInfo } from '@main/data/core-settings-info.data'; import { SettingNames, SettingTypes } from 'papi-shared-types'; -import { PlatformEventEmitter, UnsubscriberAsync, serialize } from 'platform-bible-utils'; +import { + PlatformEventEmitter, + UnsubscriberAsync, + deserialize, + serialize, +} from 'platform-bible-utils'; +import { joinUriPaths } from '@node/utils/util'; // TODO: 3 Fix function declarations to match service-model // TODO: 4 Fix implementation of all functions -// TODO: Where is data stored, how is dp going to access it +// TODO: Where do settings live (JSON obj/file)? How is dp going to access it? class SettingDataProviderEngine extends DataProviderEngine> implements IDataProviderEngine> { - // Where do our settings live? Some JSON object/file/whatever? - + private settingsInfo; // eslint-disable-next-line @typescript-eslint/no-useless-constructor - constructor() { + constructor(settingsInfo: Partial) { super(); + this.settingsInfo = settingsInfo; } + // eslint-disable-next-line class-methods-use-this async getSetting( key: SettingName, - defaultSetting: SettingTypes[SettingName], - ): Promise => { + // defaultSetting: SettingTypes[SettingName], + ): Promise { const settingString = localStorage.getItem(key); // Null is used by the external API // eslint-disable-next-line no-null/no-null if (settingString !== null) { return deserialize(settingString); } - return defaultSetting; + const settingInfo = this.settingsInfo[key]; + if (!settingInfo) throw new Error('no setting'); + return settingInfo.default; } + // eslint-disable-next-line class-methods-use-this async setSetting( key: SettingName, newSetting: SettingTypes[SettingName], - ) => { + ): Promise>> { localStorage.setItem(key, serialize(newSetting)); // Assert type of the particular SettingName of the emitter. // eslint-disable-next-line no-type-assertion/no-type-assertion @@ -44,21 +68,27 @@ class SettingDataProviderEngine type: 'update-setting', }; emitter?.emit(setMessage); + return true; } - async resetSetting(key: SettingName) => { + // eslint-disable-next-line class-methods-use-this + async resetSetting( + key: SettingName, + ): Promise>> { localStorage.removeItem(key); // Assert type of the particular SettingName of the emitter. // eslint-disable-next-line no-type-assertion/no-type-assertion const emitter = onDidUpdateSettingEmitters.get(key); const resetMessage: ResetSettingEvent = { type: 'reset-setting' }; emitter?.emit(resetMessage); + return true; } - async subscribeToSetting( + // eslint-disable-next-line class-methods-use-this + async subscribeSetting( key: SettingName, callback: (newSetting: SettingEvent) => void, - ): Promise => { + ): Promise { // Assert type of the particular SettingName of the emitter. // eslint-disable-next-line no-type-assertion/no-type-assertion let emitter = onDidUpdateSettingEmitters.get(key) as @@ -75,6 +105,16 @@ class SettingDataProviderEngine } return emitter.subscribe(callback); } + + // eslint-disable-next-line class-methods-use-this + async #loadSettings(): Promise { + const settingString = await nodeFS.readFileText( + joinUriPaths('shared', 'data', 'settings-values.ts'), + ); + if (!settingString) throw new Error('Error reading settings'); + const settingsObj: SettingsValues = deserialize(settingString); + return settingsObj; + } } let initializationPromise: Promise; @@ -87,7 +127,7 @@ export async function initialize(): Promise { try { dataProvider = await dataProviderService.registerEngine( settingsServiceDataProviderName, - new SettingDataProviderEngine(), // will be fixed when dp types are correct + new SettingDataProviderEngine(coreSettingsInfo), // will be fixed when dp types are correct ); resolve(); } catch (error) { diff --git a/src/shared/services/settings.service-model.ts b/src/shared/services/settings.service-model.ts index 54fdd48a69..8c08b2d936 100644 --- a/src/shared/services/settings.service-model.ts +++ b/src/shared/services/settings.service-model.ts @@ -2,23 +2,27 @@ import { SettingNames, SettingTypes } from 'papi-shared-types'; import { OnDidDispose, PlatformEventEmitter, UnsubscriberAsync } from 'platform-bible-utils'; import { DataProviderDataType, + DataProviderSubscriberOptions, DataProviderUpdateInstructions, IDataProvider, } from './papi-core.service'; -/** - * Name used to register the data provider - * - * You can use this name - */ +/** JSDOC DESTINATION dataProviderName */ export const settingsServiceDataProviderName = 'platform.settingsServiceDataProvider'; export const settingsServiceObjectToProxy = Object.freeze({ + /** + * JSDOC SOURCE dataProviderName + * + * Name used to register the data provider + * + * You can use this name + */ dataProviderName: settingsServiceDataProviderName, }); // TODO: 1 Fix types- should they be like string/T or SettingName extends SettingNames -export type SettingDataTypes = { - Setting: DataProviderDataType; +export type SettingDataTypes = { + // '': DataProviderDataType; }; declare module 'papi-shared-types' { @@ -61,7 +65,7 @@ export type ISettingsService = { * @returns The value of the specified setting, parsed to an object. Returns default setting if * setting does not exist */ - getSetting( + get( key: SettingName, defaultSetting: SettingTypes[SettingName], ): Promise; @@ -73,19 +77,18 @@ export type ISettingsService = { * @param newSetting The value that is to be stored. Setting the new value to `undefined` is the * equivalent of deleting the setting */ - setSetting( + set( key: SettingName, newSetting: SettingTypes[SettingName], - ): Promise>>; + ): Promise>; /** * Removes the setting from memory * * @param key The string id of the setting for which the value is being removed + * @returns `true` if successfully reset the project setting. `false` otherwise */ - resetSetting( - key: SettingName, - ): Promise>>; + reset(key: SettingName): Promise; /** * Subscribes to updates of the specified setting. Whenever the value of the setting changes, the @@ -95,10 +98,40 @@ export type ISettingsService = { * @param callback The function that will be called whenever the specified setting is updated * @returns Unsubscriber that should be called whenever the subscription should be deleted */ - subscribeSetting( + subscribe( key: SettingName, callback: (newSetting: SettingEvent) => void, + options?: DataProviderSubscriberOptions, ): Promise; } & OnDidDispose & - IDataProvider> & + IDataProvider & typeof settingsServiceObjectToProxy; + +// eslint-disable-next-line no-type-assertion/no-type-assertion +const blah = {} as ISettingsService; +const thing = await blah.get('platform.verseRef'); + +// TODO: delete this utility function once menuDataService is pushed- don't have access to it now +export function createSyncProxyForAsyncObject( + getObject: (args?: unknown[]) => Promise, + objectToProxy: Partial = {}, +): T { + // objectToProxy will have only the synchronously accessed properties of T on it, and this proxy + // makes the async methods that do not exist yet available synchronously so we have all of T + // eslint-disable-next-line no-type-assertion/no-type-assertion + return new Proxy(objectToProxy as T, { + get(target, prop) { + // We don't have any type information for T, so we assume methodName exists on it and will let JavaScript throw if it doesn't exist + // @ts-expect-error 7053 + if (prop in target) return target[prop]; + return async (...args: unknown[]) => { + // 7053: We don't have any type information for T, so we assume methodName exists on it and will let JavaScript throw if it doesn't exist + // 2556: The args here are the parameters for the method specified + // @ts-expect-error 7053 2556 + return (await getObject())[prop](...args); + }; + }, + }); +} + +export type SettingsValues = { [settingId: string]: unknown }; diff --git a/src/shared/services/settings.service.ts b/src/shared/services/settings.service.ts index c661f3e09a..5f17bb9555 100644 --- a/src/shared/services/settings.service.ts +++ b/src/shared/services/settings.service.ts @@ -1,5 +1,6 @@ import dataProviderService from './data-provider.service'; import { + createSyncProxyForAsyncObject, ISettingsService, settingsServiceDataProviderName, settingsServiceObjectToProxy, @@ -26,29 +27,6 @@ async function initialize(): Promise { return initializationPromise; } -// TODO: delete this utility function once menuDataService is pushed- don't have access to it now -function createSyncProxyForAsyncObject( - getObject: (args?: unknown[]) => Promise, - objectToProxy: Partial = {}, -): T { - // objectToProxy will have only the synchronously accessed properties of T on it, and this proxy - // makes the async methods that do not exist yet available synchronously so we have all of T - // eslint-disable-next-line no-type-assertion/no-type-assertion - return new Proxy(objectToProxy as T, { - get(target, prop) { - // We don't have any type information for T, so we assume methodName exists on it and will let JavaScript throw if it doesn't exist - // @ts-expect-error 7053 - if (prop in target) return target[prop]; - return async (...args: unknown[]) => { - // 7053: We don't have any type information for T, so we assume methodName exists on it and will let JavaScript throw if it doesn't exist - // 2556: The args here are the parameters for the method specified - // @ts-expect-error 7053 2556 - return (await getObject())[prop](...args); - }; - }, - }); -} - const menuDataService = createSyncProxyForAsyncObject(async () => { await initialize(); return dataProvider; From 4f8d764b0ca1b5e8e00eb841df69bb1281df3f56 Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Tue, 6 Feb 2024 16:26:01 -0500 Subject: [PATCH 04/19] Updates to settings service and useSetting hook --- lib/papi-dts/papi.d.ts | 8147 ++++++++--------- src/main/services/settings.service-host.ts | 40 +- .../hooks/papi-hooks/use-setting.hook.ts | 61 +- src/shared/services/settings.service-model.ts | 63 +- src/shared/services/settings.service.ts | 8 +- 5 files changed, 3684 insertions(+), 4635 deletions(-) diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index bb0a789de9..0c21bb8b19 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -1,249 +1,166 @@ /// /// /// -declare module 'shared/models/web-view.model' { - /** The type of code that defines a webview's content */ - export enum WebViewContentType { - /** - * This webview is a React webview. It must specify its component by setting it to - * `globalThis.webViewComponent` - */ - React = 'react', - /** This webview is a raw HTML/JS/CSS webview. */ - HTML = 'html', - /** - * This webview's content is fetched from the url specified (iframe `src` attribute). Note that - * webViews of this type cannot access the `papi` because they cannot be on the same origin as the - * parent window. - */ - URL = 'url', - } - /** What type a WebView is. Each WebView definition must have a unique type. */ - export type WebViewType = string; - /** ID for a specific WebView. Each WebView has a unique ID */ - export type WebViewId = string; - /** Base WebView properties that all WebViews share */ - type WebViewDefinitionBase = { - /** What type of WebView this is. Unique to all other WebView definitions */ - webViewType: WebViewType; - /** Unique ID among webviews specific to this webview instance. */ - id: WebViewId; - /** The code for the WebView that papi puts into an iframe */ - content: string; - /** - * Url of image to show on the title bar of the tab - * - * Defaults to Platform.Bible logo - */ - iconUrl?: string; - /** Name of the tab for the WebView */ - title?: string; - /** Tooltip that is shown when hovering over the webview title */ - tooltip?: string; - /** General object to store unique state for this webview */ - state?: Record; - /** - * Whether to allow the WebView iframe to interact with its parent as a same-origin website. - * Setting this to true adds `allow-same-origin` to the WebView iframe's [sandbox attribute] - * (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox). Defaults to - * `true`. - * - * Setting this to false on an HTML or React WebView prevents the iframe from importing the `papi` - * and such and also prevents others from accessing its document. This could be useful when you - * need secure input from the user because other WebViews may be able to attach event listeners to - * your inputs if you are on the same origin. Setting this to `false` on HTML or React WebViews is - * a big security win, but it makes interacting with the platform more challenging in some ways. - * - * Setting this to false on a URL WebView prevents the iframe from accessing same-origin features - * on its host website like storage APIs (localstorage, cookies, etc) and such. This will likely - * break many websites. - * - * It is best practice to set this to `false` where possible. - * - * Note: Until we have a message-passing API for WebViews, there is currently no way to interact - * with the platform via a WebView with `allowSameOrigin: false`. - * - * WARNING: If your WebView accepts secure user input like passwords on HTML or React WebViews, - * you MUST set this to `false` or you will risk exposing that secure input to other extensions - * who could be phishing for it. - */ - allowSameOrigin?: boolean; - /** - * Whether to allow scripts to run in this iframe. Setting this to true adds `allow-scripts` to - * the WebView iframe's [sandbox attribute] - * (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox). Defaults to `true` - * for HTML and React WebViews and `false` for URL WebViews - * - * WARNING: Setting this to `true` increases the possibility of a security threat occurring. If it - * is not necessary to run scripts in your WebView, you should set this to `false` to reduce - * risk. - */ - allowScripts?: boolean; - /** - * **For HTML and React WebViews:** List of [Host or scheme - * values](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#hosts_values) - * to include in the [`frame-src` - * directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src) - * for this WebView's iframe content-security-policy. This allows iframes with `src` attributes - * matching these host values to be loaded in your WebView. You can only specify values starting - * with `papi-extension:` and `https:`; any others are ignored. Specifying urls in this array - * whitelists those urls so you can embed iframes with those urls as the `src` attribute. By - * default, no urls are available to be iframes. If you want to embed iframes with the `src` - * attribute in your webview, you must include them in this property. - * - * For example, if you specify `allowFrameSources: ['https://example.com/']`, you will be able to - * embed iframes with urls starting with `papi-extension:` and on the same host as - * `https://example.com/` - * - * If you plan on embedding any iframes in your WebView, it is best practice to list only the host - * values you need to function. The more you list, the higher the theoretical security risks. - * - * --- - * - * **For URL WebViews:** List of strings representing RegExp patterns (passed into [the RegExp - * constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp)) - * to match against the `content` url specified (using the - * [`test`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test) - * function) to determine whether this iframe will be allowed to load. Specifying urls in this - * array is essentially a security check to make sure the url you pass is one of the urls you - * intend it to be. By default, the url you specify in `content` will be accepted (you do not have - * to specify this unless you want to, but it is recommended in some scenarios). - * - * Note: URL WebViews must have `papi-extension:` or `https:` urls. This property does not - * override that requirement. - * - * For example, if you specify `allowFrameSources: ['^papi-extension:', - * '^https://example\\.com.*']`, only `papi-extension:` and `https://example.com` urls will be - * accepted. - * - * If your WebView url is a `const` string and cannot change for any reason, you do not need to - * specify this property. However, if your WebView url is dynamic and can change in any way, it is - * best practice to specify this property and to list only the urls you need for your URL WebView - * to function. The more you list, the higher the theoretical security risks. - */ - allowedFrameSources?: string[]; - }; - /** WebView representation using React */ - export type WebViewDefinitionReact = WebViewDefinitionBase & { - /** Indicates this WebView uses React */ - contentType?: WebViewContentType.React; - /** String of styles to be loaded into the iframe for this WebView */ - styles?: string; - }; - /** WebView representation using HTML */ - export type WebViewDefinitionHtml = WebViewDefinitionBase & { - /** Indicates this WebView uses HTML */ - contentType: WebViewContentType.HTML; - }; - /** - * WebView representation using a URL. - * - * Note: you can only use `papi-extension:` and `https:` urls - */ - export type WebViewDefinitionURL = WebViewDefinitionBase & { - /** Indicates this WebView uses a URL */ - contentType: WebViewContentType.URL; - }; - /** Properties defining a type of WebView created by extensions to show web content */ - export type WebViewDefinition = - | WebViewDefinitionReact - | WebViewDefinitionHtml - | WebViewDefinitionURL; - /** - * Saved WebView information that does not contain the actual content of the WebView. Saved into - * layouts. Could have as little as the type and ID. WebView providers load these into actual - * {@link WebViewDefinition}s and verify any existing properties on the WebViews. - */ - export type SavedWebViewDefinition = ( - | Partial> - | Partial> - | Partial> - ) & - Pick; - /** The properties on a WebViewDefinition that may be updated when that webview is already displayed */ - export type WebViewDefinitionUpdatableProperties = Pick< - WebViewDefinitionBase, - 'iconUrl' | 'title' | 'tooltip' - >; - /** - * WebViewDefinition properties for updating a WebView that is already displayed. Any unspecified - * properties will stay the same - */ - export type WebViewDefinitionUpdateInfo = Partial; - /** - * - * A React hook for working with a state object tied to a webview. Returns a WebView state value and - * a function to set it. Use similarly to `useState`. - * - * Only used in WebView iframes. - * - * _@param_ `stateKey` Key of the state value to use. The webview state holds a unique value per - * key. - * - * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be - * updated every render - * - * _@param_ `defaultStateValue` Value to use if the web view state didn't contain a value for the - * given 'stateKey' - * - * Note: this parameter is internally assigned to a `ref`, so changing it will not cause any hooks - * to re-run with its new value. Running `resetWebViewState()` will always update the state value - * returned to the latest `defaultStateValue`, and changing the `stateKey` will use the latest - * `defaultStateValue`. However, if `defaultStateValue` is changed while a state is - * `defaultStateValue` (meaning it is reset and has no value), the returned state value will not be - * updated to the new `defaultStateValue`. - * - * _@returns_ `[stateValue, setStateValue, resetWebViewState]` - * - * - `webViewStateValue`: The current value for the web view state at the key specified or - * `defaultStateValue` if a state was not found - * - `setWebViewState`: Function to use to update the web view state value at the key specified - * - `resetWebViewState`: Function that removes the web view state and resets the value to - * `defaultStateValue` - * - * _@example_ - * - * ```typescript - * const [lastPersonSeen, setLastPersonSeen] = useWebViewState('lastSeen', 'No one'); - * ``` - */ - export type UseWebViewStateHook = ( - stateKey: string, - defaultStateValue: T, - ) => [ - webViewStateValue: T, - setWebViewState: (stateValue: T) => void, - resetWebViewState: () => void, - ]; - /** - * - * Gets the updatable properties on this WebView's WebView definition - * - * _@returns_ updatable properties this WebView's WebView definition or undefined if not found for - * some reason - */ - export type GetWebViewDefinitionUpdatableProperties = () => - | WebViewDefinitionUpdatableProperties - | undefined; - /** - * - * Updates this WebView with the specified properties - * - * _@param_ `updateInfo` properties to update on the WebView. Any unspecified properties will stay - * the same - * - * _@returns_ true if successfully found the WebView to update; false otherwise - * - * _@example_ - * - * ```typescript - * updateWebViewDefinition({ title: `Hello ${name}` }); - * ``` - */ - export type UpdateWebViewDefinition = (updateInfo: WebViewDefinitionUpdateInfo) => boolean; - /** Props that are passed into the web view itself inside the iframe in the web view tab component */ - export type WebViewProps = { +declare module "shared/models/web-view.model" { + /** The type of code that defines a webview's content */ + export enum WebViewContentType { + /** + * This webview is a React webview. It must specify its component by setting it to + * `globalThis.webViewComponent` + */ + React = "react", + /** This webview is a raw HTML/JS/CSS webview. */ + HTML = "html", + /** + * This webview's content is fetched from the url specified (iframe `src` attribute). Note that + * webViews of this type cannot access the `papi` because they cannot be on the same origin as the + * parent window. + */ + URL = "url" + } + /** What type a WebView is. Each WebView definition must have a unique type. */ + export type WebViewType = string; + /** ID for a specific WebView. Each WebView has a unique ID */ + export type WebViewId = string; + /** Base WebView properties that all WebViews share */ + type WebViewDefinitionBase = { + /** What type of WebView this is. Unique to all other WebView definitions */ + webViewType: WebViewType; + /** Unique ID among webviews specific to this webview instance. */ + id: WebViewId; + /** The code for the WebView that papi puts into an iframe */ + content: string; + /** + * Url of image to show on the title bar of the tab + * + * Defaults to Platform.Bible logo + */ + iconUrl?: string; + /** Name of the tab for the WebView */ + title?: string; + /** Tooltip that is shown when hovering over the webview title */ + tooltip?: string; + /** General object to store unique state for this webview */ + state?: Record; + /** + * Whether to allow the WebView iframe to interact with its parent as a same-origin website. + * Setting this to true adds `allow-same-origin` to the WebView iframe's [sandbox attribute] + * (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox). Defaults to + * `true`. + * + * Setting this to false on an HTML or React WebView prevents the iframe from importing the `papi` + * and such and also prevents others from accessing its document. This could be useful when you + * need secure input from the user because other WebViews may be able to attach event listeners to + * your inputs if you are on the same origin. Setting this to `false` on HTML or React WebViews is + * a big security win, but it makes interacting with the platform more challenging in some ways. + * + * Setting this to false on a URL WebView prevents the iframe from accessing same-origin features + * on its host website like storage APIs (localstorage, cookies, etc) and such. This will likely + * break many websites. + * + * It is best practice to set this to `false` where possible. + * + * Note: Until we have a message-passing API for WebViews, there is currently no way to interact + * with the platform via a WebView with `allowSameOrigin: false`. + * + * WARNING: If your WebView accepts secure user input like passwords on HTML or React WebViews, + * you MUST set this to `false` or you will risk exposing that secure input to other extensions + * who could be phishing for it. + */ + allowSameOrigin?: boolean; + /** + * Whether to allow scripts to run in this iframe. Setting this to true adds `allow-scripts` to + * the WebView iframe's [sandbox attribute] + * (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox). Defaults to `true` + * for HTML and React WebViews and `false` for URL WebViews + * + * WARNING: Setting this to `true` increases the possibility of a security threat occurring. If it + * is not necessary to run scripts in your WebView, you should set this to `false` to reduce + * risk. + */ + allowScripts?: boolean; + /** + * **For HTML and React WebViews:** List of [Host or scheme + * values](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#hosts_values) + * to include in the [`frame-src` + * directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src) + * for this WebView's iframe content-security-policy. This allows iframes with `src` attributes + * matching these host values to be loaded in your WebView. You can only specify values starting + * with `papi-extension:` and `https:`; any others are ignored. Specifying urls in this array + * whitelists those urls so you can embed iframes with those urls as the `src` attribute. By + * default, no urls are available to be iframes. If you want to embed iframes with the `src` + * attribute in your webview, you must include them in this property. + * + * For example, if you specify `allowFrameSources: ['https://example.com/']`, you will be able to + * embed iframes with urls starting with `papi-extension:` and on the same host as + * `https://example.com/` + * + * If you plan on embedding any iframes in your WebView, it is best practice to list only the host + * values you need to function. The more you list, the higher the theoretical security risks. + * + * --- + * + * **For URL WebViews:** List of strings representing RegExp patterns (passed into [the RegExp + * constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp)) + * to match against the `content` url specified (using the + * [`test`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test) + * function) to determine whether this iframe will be allowed to load. Specifying urls in this + * array is essentially a security check to make sure the url you pass is one of the urls you + * intend it to be. By default, the url you specify in `content` will be accepted (you do not have + * to specify this unless you want to, but it is recommended in some scenarios). + * + * Note: URL WebViews must have `papi-extension:` or `https:` urls. This property does not + * override that requirement. + * + * For example, if you specify `allowFrameSources: ['^papi-extension:', + * '^https://example\\.com.*']`, only `papi-extension:` and `https://example.com` urls will be + * accepted. + * + * If your WebView url is a `const` string and cannot change for any reason, you do not need to + * specify this property. However, if your WebView url is dynamic and can change in any way, it is + * best practice to specify this property and to list only the urls you need for your URL WebView + * to function. The more you list, the higher the theoretical security risks. + */ + allowedFrameSources?: string[]; + }; + /** WebView representation using React */ + export type WebViewDefinitionReact = WebViewDefinitionBase & { + /** Indicates this WebView uses React */ + contentType?: WebViewContentType.React; + /** String of styles to be loaded into the iframe for this WebView */ + styles?: string; + }; + /** WebView representation using HTML */ + export type WebViewDefinitionHtml = WebViewDefinitionBase & { + /** Indicates this WebView uses HTML */ + contentType: WebViewContentType.HTML; + }; + /** + * WebView representation using a URL. + * + * Note: you can only use `papi-extension:` and `https:` urls + */ + export type WebViewDefinitionURL = WebViewDefinitionBase & { + /** Indicates this WebView uses a URL */ + contentType: WebViewContentType.URL; + }; + /** Properties defining a type of WebView created by extensions to show web content */ + export type WebViewDefinition = WebViewDefinitionReact | WebViewDefinitionHtml | WebViewDefinitionURL; /** + * Saved WebView information that does not contain the actual content of the WebView. Saved into + * layouts. Could have as little as the type and ID. WebView providers load these into actual + * {@link WebViewDefinition}s and verify any existing properties on the WebViews. + */ + export type SavedWebViewDefinition = (Partial> | Partial> | Partial>) & Pick; + /** The properties on a WebViewDefinition that may be updated when that webview is already displayed */ + export type WebViewDefinitionUpdatableProperties = Pick; + /** + * WebViewDefinition properties for updating a WebView that is already displayed. Any unspecified + * properties will stay the same + */ + export type WebViewDefinitionUpdateInfo = Partial; + /** + * JSDOC SOURCE UseWebViewStateHook * * A React hook for working with a state object tied to a webview. Returns a WebView state value and * a function to set it. Use similarly to `useState`. @@ -280,16 +197,22 @@ declare module 'shared/models/web-view.model' { * const [lastPersonSeen, setLastPersonSeen] = useWebViewState('lastSeen', 'No one'); * ``` */ - useWebViewState: UseWebViewStateHook; + export type UseWebViewStateHook = (stateKey: string, defaultStateValue: T) => [ + webViewStateValue: T, + setWebViewState: (stateValue: T) => void, + resetWebViewState: () => void + ]; /** + * JSDOC SOURCE GetWebViewDefinitionUpdatableProperties * * Gets the updatable properties on this WebView's WebView definition * * _@returns_ updatable properties this WebView's WebView definition or undefined if not found for * some reason */ - getWebViewDefinitionUpdatableProperties: GetWebViewDefinitionUpdatableProperties; + export type GetWebViewDefinitionUpdatableProperties = () => WebViewDefinitionUpdatableProperties | undefined; /** + * JSDOC SOURCE UpdateWebViewDefinition * * Updates this WebView with the specified properties * @@ -304,434 +227,795 @@ declare module 'shared/models/web-view.model' { * updateWebViewDefinition({ title: `Hello ${name}` }); * ``` */ - updateWebViewDefinition: UpdateWebViewDefinition; - }; - /** Options that affect what `webViews.getWebView` does */ - export type GetWebViewOptions = { + export type UpdateWebViewDefinition = (updateInfo: WebViewDefinitionUpdateInfo) => boolean; + /** Props that are passed into the web view itself inside the iframe in the web view tab component */ + export type WebViewProps = { + /** JSDOC DESTINATION UseWebViewStateHook */ + useWebViewState: UseWebViewStateHook; + /** JSDOC DESTINATION GetWebViewDefinitionUpdatableProperties */ + getWebViewDefinitionUpdatableProperties: GetWebViewDefinitionUpdatableProperties; + /** JSDOC DESTINATION UpdateWebViewDefinition */ + updateWebViewDefinition: UpdateWebViewDefinition; + }; + /** Options that affect what `webViews.getWebView` does */ + export type GetWebViewOptions = { + /** + * If provided and if a web view with this ID exists, requests from the web view provider an + * existing WebView with this ID if one exists. The web view provider can deny the request if it + * chooses to do so. + * + * Alternatively, set this to '?' to attempt to find any existing web view with the specified + * webViewType. + * + * Note: setting `existingId` to `undefined` counts as providing in this case (providing is tested + * with `'existingId' in options`, not just testing if `existingId` is truthy). Not providing an + * `existingId` at all is the only way to specify we are not looking for an existing webView + */ + existingId?: string | '?' | undefined; + /** + * Whether to create a webview with a new ID and a webview with ID `existingId` was not found. + * Only relevant if `existingId` is provided. If `existingId` is not provided, this property is + * ignored. + * + * Defaults to true + */ + createNewIfNotFound?: boolean; + }; +} +declare module "shared/global-this.model" { + import { LogLevel } from 'electron-log'; + import { FunctionComponent } from 'react'; + import { GetWebViewDefinitionUpdatableProperties, UpdateWebViewDefinition, UseWebViewStateHook, WebViewDefinitionUpdatableProperties, WebViewDefinitionUpdateInfo, WebViewProps } from "shared/models/web-view.model"; + /** + * Variables that are defined in global scope. These must be defined in main.ts (main), index.ts + * (renderer), and extension-host.ts (extension host) + */ + global { + /** Type of process this is. Helps with running specific code based on which process you're in */ + var processType: ProcessType; + /** Whether this process is packaged or running from sources */ + var isPackaged: boolean; + /** + * Path to the app's resources directory. This is a string representation of the resources uri on + * frontend + */ + var resourcesPath: string; + /** How much logging should be recorded. Defaults to 'debug' if not packaged, 'info' if packaged */ + var logLevel: LogLevel; + /** + * A function that each React WebView extension must provide for Paranext to display it. Only used + * in WebView iframes. + */ + var webViewComponent: FunctionComponent; + /** JSDOC DESTINATION UseWebViewStateHook */ + var useWebViewState: UseWebViewStateHook; + /** + * Retrieve the value from web view state with the given 'stateKey', if it exists. Otherwise + * return default value + */ + var getWebViewState: (stateKey: string, defaultValue: T) => T; + /** Set the value for a given key in the web view state. */ + var setWebViewState: (stateKey: string, stateValue: T) => void; + /** Remove the value for a given key in the web view state */ + var resetWebViewState: (stateKey: string) => void; + var getWebViewDefinitionUpdatablePropertiesById: (webViewId: string) => WebViewDefinitionUpdatableProperties | undefined; + var updateWebViewDefinitionById: (webViewId: string, webViewDefinitionUpdateInfo: WebViewDefinitionUpdateInfo) => boolean; + /** JSDOC DESTINATION GetWebViewDefinitionUpdatableProperties */ + var getWebViewDefinitionUpdatableProperties: GetWebViewDefinitionUpdatableProperties; + /** JSDOC DESTINATION UpdateWebViewDefinition */ + var updateWebViewDefinition: UpdateWebViewDefinition; + } + /** Type of Paranext process */ + export enum ProcessType { + Main = "main", + Renderer = "renderer", + ExtensionHost = "extension-host" + } +} +declare module "shared/utils/util" { + import { ProcessType } from "shared/global-this.model"; + import { UnsubscriberAsync } from 'platform-bible-utils'; /** - * If provided and if a web view with this ID exists, requests from the web view provider an - * existing WebView with this ID if one exists. The web view provider can deny the request if it - * chooses to do so. + * Create a nonce that is at least 128 bits long and should be (is not currently) cryptographically + * random. See nonce spec at https://w3c.github.io/webappsec-csp/#security-nonces * - * Alternatively, set this to '?' to attempt to find any existing web view with the specified - * webViewType. - * - * Note: setting `existingId` to `undefined` counts as providing in this case (providing is tested - * with `'existingId' in options`, not just testing if `existingId` is truthy). Not providing an - * `existingId` at all is the only way to specify we are not looking for an existing webView + * WARNING: THIS IS NOT CURRENTLY CRYPTOGRAPHICALLY SECURE! TODO: Make this cryptographically + * random! Use some polymorphic library that works in all contexts? + * https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues only works in browser */ - existingId?: string | '?' | undefined; + export function newNonce(): string; /** - * Whether to create a webview with a new ID and a webview with ID `existingId` was not found. - * Only relevant if `existingId` is provided. If `existingId` is not provided, this property is - * ignored. + * Creates a safe version of a register function that returns a Promise. * - * Defaults to true + * @param unsafeRegisterFn Function that does some kind of async registration and returns an + * unsubscriber and a promise that resolves when the registration is finished + * @param isInitialized Whether the service associated with this safe UnsubscriberAsync function is + * initialized + * @param initialize Promise that resolves when the service is finished initializing + * @returns Safe version of an unsafe function that returns a promise to an UnsubscriberAsync + * (meaning it will wait to register until the service is initialized) */ - createNewIfNotFound?: boolean; - }; -} -declare module 'shared/global-this.model' { - import { LogLevel } from 'electron-log'; - import { FunctionComponent } from 'react'; - import { - GetWebViewDefinitionUpdatableProperties, - UpdateWebViewDefinition, - UseWebViewStateHook, - WebViewDefinitionUpdatableProperties, - WebViewDefinitionUpdateInfo, - WebViewProps, - } from 'shared/models/web-view.model'; - /** - * Variables that are defined in global scope. These must be defined in main.ts (main), index.ts - * (renderer), and extension-host.ts (extension host) - */ - global { - /** Type of process this is. Helps with running specific code based on which process you're in */ - var processType: ProcessType; - /** Whether this process is packaged or running from sources */ - var isPackaged: boolean; - /** - * Path to the app's resources directory. This is a string representation of the resources uri on - * frontend - */ - var resourcesPath: string; - /** How much logging should be recorded. Defaults to 'debug' if not packaged, 'info' if packaged */ - var logLevel: LogLevel; - /** - * A function that each React WebView extension must provide for Paranext to display it. Only used - * in WebView iframes. - */ - var webViewComponent: FunctionComponent; + export const createSafeRegisterFn: (unsafeRegisterFn: (...args: TParam) => Promise, isInitialized: boolean, initialize: () => Promise) => (...args: TParam) => Promise; /** - * - * A React hook for working with a state object tied to a webview. Returns a WebView state value and - * a function to set it. Use similarly to `useState`. - * - * Only used in WebView iframes. - * - * _@param_ `stateKey` Key of the state value to use. The webview state holds a unique value per - * key. - * - * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be - * updated every render - * - * _@param_ `defaultStateValue` Value to use if the web view state didn't contain a value for the - * given 'stateKey' - * - * Note: this parameter is internally assigned to a `ref`, so changing it will not cause any hooks - * to re-run with its new value. Running `resetWebViewState()` will always update the state value - * returned to the latest `defaultStateValue`, and changing the `stateKey` will use the latest - * `defaultStateValue`. However, if `defaultStateValue` is changed while a state is - * `defaultStateValue` (meaning it is reset and has no value), the returned state value will not be - * updated to the new `defaultStateValue`. - * - * _@returns_ `[stateValue, setStateValue, resetWebViewState]` - * - * - `webViewStateValue`: The current value for the web view state at the key specified or - * `defaultStateValue` if a state was not found - * - `setWebViewState`: Function to use to update the web view state value at the key specified - * - `resetWebViewState`: Function that removes the web view state and resets the value to - * `defaultStateValue` - * - * _@example_ - * - * ```typescript - * const [lastPersonSeen, setLastPersonSeen] = useWebViewState('lastSeen', 'No one'); - * ``` + * Type of object passed to a complex request handler that provides information about the request. + * This type is used as the public-facing interface for requests */ - var useWebViewState: UseWebViewStateHook; + export type ComplexRequest = { + /** The one who sent the request */ + senderId: number; + contents: TParam; + }; + type ComplexResponseSuccess = { + /** Whether the handler that created this response was successful in handling the request */ + success: true; + /** + * Content with which to respond to the request. Must be provided unless the response failed or + * TReturn is undefined + */ + contents: TReturn; + }; + type ComplexResponseFailure = { + /** Whether the handler that created this response was successful in handling the request */ + success: false; + /** + * Content with which to respond to the request. Must be provided unless the response failed or + * TReturn is undefined Removed from failure so we do not change the type of contents for type + * safety. We could add errorContents one day if we really need it + */ + /** Error explaining the problem that is only populated if success is false */ + errorMessage: string; + }; /** - * Retrieve the value from web view state with the given 'stateKey', if it exists. Otherwise - * return default value + * Type of object to create when handling a complex request where you desire to provide additional + * information beyond the contents of the response This type is used as the public-facing interface + * for responses */ - var getWebViewState: (stateKey: string, defaultValue: T) => T; - /** Set the value for a given key in the web view state. */ - var setWebViewState: (stateKey: string, stateValue: T) => void; - /** Remove the value for a given key in the web view state */ - var resetWebViewState: (stateKey: string) => void; - var getWebViewDefinitionUpdatablePropertiesById: ( - webViewId: string, - ) => WebViewDefinitionUpdatableProperties | undefined; - var updateWebViewDefinitionById: ( - webViewId: string, - webViewDefinitionUpdateInfo: WebViewDefinitionUpdateInfo, - ) => boolean; + export type ComplexResponse = ComplexResponseSuccess | ComplexResponseFailure; + /** Type of request handler - indicates what type of parameters and what return type the handler has */ + export enum RequestHandlerType { + Args = "args", + Contents = "contents", + Complex = "complex" + } /** + * Modules that someone might try to require in their extensions that we have similar apis for. When + * an extension requires these modules, an error throws that lets them know about our similar api. + */ + export const MODULE_SIMILAR_APIS: Readonly<{ + [moduleName: string]: string | { + [process in ProcessType | 'default']?: string; + } | undefined; + }>; + /** + * Get a message that says the module import was rejected and to try a similar api if available. + * + * @param moduleName Name of `require`d module that was rejected + * @returns String that says the import was rejected and a similar api to try + */ + export function getModuleSimilarApiMessage(moduleName: string): string; + /** Separator between parts of a serialized request */ + const REQUEST_TYPE_SEPARATOR = ":"; + /** Information about a request that tells us what to do with it */ + export type RequestType = { + /** The general category of request */ + category: string; + /** Specific identifier for this type of request */ + directive: string; + }; + /** + * String version of a request type that tells us what to do with a request. * - * Gets the updatable properties on this WebView's WebView definition - * - * _@returns_ updatable properties this WebView's WebView definition or undefined if not found for - * some reason + * Consists of two strings concatenated by a colon */ - var getWebViewDefinitionUpdatableProperties: GetWebViewDefinitionUpdatableProperties; + export type SerializedRequestType = `${string}${typeof REQUEST_TYPE_SEPARATOR}${string}`; /** + * Create a request message requestType string from a category and a directive * - * Updates this WebView with the specified properties - * - * _@param_ `updateInfo` properties to update on the WebView. Any unspecified properties will stay - * the same - * - * _@returns_ true if successfully found the WebView to update; false otherwise - * - * _@example_ - * - * ```typescript - * updateWebViewDefinition({ title: `Hello ${name}` }); - * ``` + * @param category The general category of request + * @param directive Specific identifier for this type of request + * @returns Full requestType for use in network calls */ - var updateWebViewDefinition: UpdateWebViewDefinition; - } - /** Type of Paranext process */ - export enum ProcessType { - Main = 'main', - Renderer = 'renderer', - ExtensionHost = 'extension-host', - } -} -declare module 'shared/utils/util' { - import { ProcessType } from 'shared/global-this.model'; - import { UnsubscriberAsync } from 'platform-bible-utils'; - /** - * Create a nonce that is at least 128 bits long and should be (is not currently) cryptographically - * random. See nonce spec at https://w3c.github.io/webappsec-csp/#security-nonces - * - * WARNING: THIS IS NOT CURRENTLY CRYPTOGRAPHICALLY SECURE! TODO: Make this cryptographically - * random! Use some polymorphic library that works in all contexts? - * https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues only works in browser - */ - export function newNonce(): string; - /** - * Creates a safe version of a register function that returns a Promise. - * - * @param unsafeRegisterFn Function that does some kind of async registration and returns an - * unsubscriber and a promise that resolves when the registration is finished - * @param isInitialized Whether the service associated with this safe UnsubscriberAsync function is - * initialized - * @param initialize Promise that resolves when the service is finished initializing - * @returns Safe version of an unsafe function that returns a promise to an UnsubscriberAsync - * (meaning it will wait to register until the service is initialized) - */ - export const createSafeRegisterFn: ( - unsafeRegisterFn: (...args: TParam) => Promise, - isInitialized: boolean, - initialize: () => Promise, - ) => (...args: TParam) => Promise; - /** - * Type of object passed to a complex request handler that provides information about the request. - * This type is used as the public-facing interface for requests - */ - export type ComplexRequest = { - /** The one who sent the request */ - senderId: number; - contents: TParam; - }; - type ComplexResponseSuccess = { - /** Whether the handler that created this response was successful in handling the request */ - success: true; - /** - * Content with which to respond to the request. Must be provided unless the response failed or - * TReturn is undefined - */ - contents: TReturn; - }; - type ComplexResponseFailure = { - /** Whether the handler that created this response was successful in handling the request */ - success: false; - /** - * Content with which to respond to the request. Must be provided unless the response failed or - * TReturn is undefined Removed from failure so we do not change the type of contents for type - * safety. We could add errorContents one day if we really need it - */ - /** Error explaining the problem that is only populated if success is false */ - errorMessage: string; - }; - /** - * Type of object to create when handling a complex request where you desire to provide additional - * information beyond the contents of the response This type is used as the public-facing interface - * for responses - */ - export type ComplexResponse = - | ComplexResponseSuccess - | ComplexResponseFailure; - /** Type of request handler - indicates what type of parameters and what return type the handler has */ - export enum RequestHandlerType { - Args = 'args', - Contents = 'contents', - Complex = 'complex', - } - /** - * Modules that someone might try to require in their extensions that we have similar apis for. When - * an extension requires these modules, an error throws that lets them know about our similar api. - */ - export const MODULE_SIMILAR_APIS: Readonly<{ - [moduleName: string]: - | string - | { - [process in ProcessType | 'default']?: string; - } - | undefined; - }>; - /** - * Get a message that says the module import was rejected and to try a similar api if available. - * - * @param moduleName Name of `require`d module that was rejected - * @returns String that says the import was rejected and a similar api to try - */ - export function getModuleSimilarApiMessage(moduleName: string): string; - /** Separator between parts of a serialized request */ - const REQUEST_TYPE_SEPARATOR = ':'; - /** Information about a request that tells us what to do with it */ - export type RequestType = { - /** The general category of request */ - category: string; - /** Specific identifier for this type of request */ - directive: string; - }; - /** - * String version of a request type that tells us what to do with a request. - * - * Consists of two strings concatenated by a colon - */ - export type SerializedRequestType = `${string}${typeof REQUEST_TYPE_SEPARATOR}${string}`; - /** - * Create a request message requestType string from a category and a directive - * - * @param category The general category of request - * @param directive Specific identifier for this type of request - * @returns Full requestType for use in network calls - */ - export function serializeRequestType(category: string, directive: string): SerializedRequestType; - /** Split a request message requestType string into its parts */ - export function deserializeRequestType(requestType: SerializedRequestType): RequestType; -} -declare module 'shared/data/internal-connection.model' { - /** - * Types that are internal to the communication we do through WebSocket. These types should not need - * to be used outside of NetworkConnectors and ConnectionService.ts - */ - import { ComplexRequest, ComplexResponse, SerializedRequestType } from 'shared/utils/util'; - /** Represents when the client id has not been assigned by the server */ - export const CLIENT_ID_UNASSIGNED = -1; - /** "Client id" for the server */ - export const CLIENT_ID_SERVER = 0; - /** Represents when the connector info has not been populated by the server */ - export const CONNECTOR_INFO_DISCONNECTED: Readonly<{ - clientId: -1; - }>; - /** Prefix on requests that indicates that the request is a command */ - export const CATEGORY_COMMAND = 'command'; - /** Information about the network connector */ - export type NetworkConnectorInfo = Readonly<{ - clientId: number; - }>; - /** Event emitted when client connections are established */ - export type ClientConnectEvent = { - clientId: number; - didReconnect: boolean; - }; - /** Event emitted when client connections are lost */ - export type ClientDisconnectEvent = { - clientId: number; - }; - /** - * Functions that run when network connector events occur. These should likely be emit functions - * from NetworkEventEmitters so the events inform all interested connections - */ - export type NetworkConnectorEventHandlers = { - /** Handles when a new connection is established */ - didClientConnectHandler?: (event: ClientConnectEvent) => void; - /** Handles when a client disconnects */ - didClientDisconnectHandler?: (event: ClientDisconnectEvent) => void; - }; - /** - * Whether this connector is setting up or has finished setting up its connection and is ready to - * communicate on the network - */ - export enum ConnectionStatus { - /** This connector is not connected to the network */ - Disconnected = 0, - /** This connector is attempting to connect to the network and retrieve connectorInfo */ - Connecting = 1, - /** This connector has finished setting up its connection - has connectorInfo and such */ - Connected = 2, - } - /** Request to do something and to respond */ - export type InternalRequest = { - requestId: number; - } & ComplexRequest; - /** Response to a request */ - export type InternalResponse = { - /** The process that sent this Response */ - senderId: number; - requestId: number; - /** The process that originally sent the Request that matches to this response */ - requesterId: number; - } & ComplexResponse; - /** - * Handler for requests from the server. Used internally between network connector and Connection - * Service - */ - export type InternalRequestHandler = ( - requestType: string, - request: InternalRequest, - ) => Promise>; - /** Handler for requests from the server */ - export type RequestHandler = ( - requestType: SerializedRequestType, - request: ComplexRequest, - ) => Promise>; - /** Function that returns a clientId to which to send the request based on the requestType */ - export type RequestRouter = (requestType: string) => number; - /** Event to be sent out throughout all processes */ - export type InternalEvent = { - /** The process that emitted this Event */ - senderId: number; - /** Contents of the event */ - event: T; - }; - /** - * Handler for events from on the network. Used internally between network connector and Connection - * Service - */ - export type InternalNetworkEventHandler = ( - eventType: string, - incomingEvent: InternalEvent, - ) => void; - /** Handler for events from on the network */ - export type NetworkEventHandler = (eventType: string, event: T) => void; + export function serializeRequestType(category: string, directive: string): SerializedRequestType; + /** Split a request message requestType string into its parts */ + export function deserializeRequestType(requestType: SerializedRequestType): RequestType; } -declare module 'shared/services/network-connector.interface' { - import { - ConnectionStatus, - InternalEvent, - InternalNetworkEventHandler, - InternalRequestHandler, - NetworkConnectorEventHandlers, - NetworkConnectorInfo, - RequestRouter, - } from 'shared/data/internal-connection.model'; - /** - * Interface that defines the network connection functionality the server and the client must - * implement. Used by NetworkConnectorFactory to supply the right kind of NetworkConnector to - * ConnectionService - */ - export default interface INetworkConnector { - /** Information about the connector. Populated by the server while connecting */ - connectorInfo: NetworkConnectorInfo; +declare module "shared/data/internal-connection.model" { + /** + * Types that are internal to the communication we do through WebSocket. These types should not need + * to be used outside of NetworkConnectors and ConnectionService.ts + */ + import { ComplexRequest, ComplexResponse, SerializedRequestType } from "shared/utils/util"; + /** Represents when the client id has not been assigned by the server */ + export const CLIENT_ID_UNASSIGNED = -1; + /** "Client id" for the server */ + export const CLIENT_ID_SERVER = 0; + /** Represents when the connector info has not been populated by the server */ + export const CONNECTOR_INFO_DISCONNECTED: Readonly<{ + clientId: -1; + }>; + /** Prefix on requests that indicates that the request is a command */ + export const CATEGORY_COMMAND = "command"; + /** Information about the network connector */ + export type NetworkConnectorInfo = Readonly<{ + clientId: number; + }>; + /** Event emitted when client connections are established */ + export type ClientConnectEvent = { + clientId: number; + didReconnect: boolean; + }; + /** Event emitted when client connections are lost */ + export type ClientDisconnectEvent = { + clientId: number; + }; + /** + * Functions that run when network connector events occur. These should likely be emit functions + * from NetworkEventEmitters so the events inform all interested connections + */ + export type NetworkConnectorEventHandlers = { + /** Handles when a new connection is established */ + didClientConnectHandler?: (event: ClientConnectEvent) => void; + /** Handles when a client disconnects */ + didClientDisconnectHandler?: (event: ClientDisconnectEvent) => void; + }; /** * Whether this connector is setting up or has finished setting up its connection and is ready to * communicate on the network */ - connectionStatus: ConnectionStatus; + export enum ConnectionStatus { + /** This connector is not connected to the network */ + Disconnected = 0, + /** This connector is attempting to connect to the network and retrieve connectorInfo */ + Connecting = 1, + /** This connector has finished setting up its connection - has connectorInfo and such */ + Connected = 2 + } + /** Request to do something and to respond */ + export type InternalRequest = { + requestId: number; + } & ComplexRequest; + /** Response to a request */ + export type InternalResponse = { + /** The process that sent this Response */ + senderId: number; + requestId: number; + /** The process that originally sent the Request that matches to this response */ + requesterId: number; + } & ComplexResponse; + /** + * Handler for requests from the server. Used internally between network connector and Connection + * Service + */ + export type InternalRequestHandler = (requestType: string, request: InternalRequest) => Promise>; + /** Handler for requests from the server */ + export type RequestHandler = (requestType: SerializedRequestType, request: ComplexRequest) => Promise>; + /** Function that returns a clientId to which to send the request based on the requestType */ + export type RequestRouter = (requestType: string) => number; + /** Event to be sent out throughout all processes */ + export type InternalEvent = { + /** The process that emitted this Event */ + senderId: number; + /** Contents of the event */ + event: T; + }; + /** + * Handler for events from on the network. Used internally between network connector and Connection + * Service + */ + export type InternalNetworkEventHandler = (eventType: string, incomingEvent: InternalEvent) => void; + /** Handler for events from on the network */ + export type NetworkEventHandler = (eventType: string, event: T) => void; +} +declare module "shared/services/network-connector.interface" { + import { ConnectionStatus, InternalEvent, InternalNetworkEventHandler, InternalRequestHandler, NetworkConnectorEventHandlers, NetworkConnectorInfo, RequestRouter } from "shared/data/internal-connection.model"; + /** + * Interface that defines the network connection functionality the server and the client must + * implement. Used by NetworkConnectorFactory to supply the right kind of NetworkConnector to + * ConnectionService + */ + export default interface INetworkConnector { + /** Information about the connector. Populated by the server while connecting */ + connectorInfo: NetworkConnectorInfo; + /** + * Whether this connector is setting up or has finished setting up its connection and is ready to + * communicate on the network + */ + connectionStatus: ConnectionStatus; + /** + * Sets up the NetworkConnector by populating connector info, setting up event handlers, and doing + * one of the following: + * + * - On Client: connecting to the server. + * - On Server: opening an endpoint for clients to connect. + * + * MUST ALSO RUN notifyClientConnected() WHEN PROMISE RESOLVES + * + * @param localRequestHandler Function that handles requests from the connection. Only called when + * this connector can handle the request + * @param requestRouter Function that returns a clientId to which to send the request based on the + * requestType. If requestRouter returns this connector's clientId, localRequestHandler is used + * @param localEventHandler Function that handles events from the server by accepting an eventType + * and an event and emitting the event locally + * @param networkConnectorEventHandlers Functions that run when network connector events occur + * like when clients are disconnected + * @returns Promise that resolves with connector info when finished connecting + */ + connect: (localRequestHandler: InternalRequestHandler, requestRouter: RequestRouter, localEventHandler: InternalNetworkEventHandler, networkConnectorEventHandlers: NetworkConnectorEventHandlers) => Promise; + /** + * Notify the server that this client has received its connectorInfo and is ready to go. + * + * MUST RUN AFTER connect() WHEN ITS PROMISE RESOLVES + * + * TODO: Is this necessary? + */ + notifyClientConnected: () => Promise; + /** + * Disconnects from the connection: + * + * - On Client: disconnects from the server + * - On Server: disconnects from clients and closes its connection endpoint + */ + disconnect: () => void; + /** + * Send a request to the server/a client and resolve after receiving a response + * + * @param requestType The type of request + * @param contents Contents to send in the request + * @returns Promise that resolves with the response message + */ + request: InternalRequestHandler; + /** + * Sends an event to other processes. Does NOT run the local event subscriptions as they should be + * run by NetworkEventEmitter after sending on network. + * + * @param eventType Unique network event type for coordinating between processes + * @param event Event to emit on the network + */ + emitEventOnNetwork: (eventType: string, event: InternalEvent) => Promise; + } +} +declare module "shared/utils/internal-util" { + /** Utility functions specific to the internal technologies we are using. */ + import { ProcessType } from "shared/global-this.model"; + /** + * Determine if running on a client process (renderer, extension-host) or on the server. + * + * @returns Returns true if running on a client, false otherwise + */ + export const isClient: () => boolean; /** - * Sets up the NetworkConnector by populating connector info, setting up event handlers, and doing - * one of the following: + * Determine if running on the server process (main) * - * - On Client: connecting to the server. - * - On Server: opening an endpoint for clients to connect. + * @returns Returns true if running on the server, false otherwise + */ + export const isServer: () => boolean; + /** + * Determine if running on the renderer process * - * MUST ALSO RUN notifyClientConnected() WHEN PROMISE RESOLVES + * @returns Returns true if running on the renderer, false otherwise + */ + export const isRenderer: () => boolean; + /** + * Determine if running on the extension host * - * @param localRequestHandler Function that handles requests from the connection. Only called when - * this connector can handle the request - * @param requestRouter Function that returns a clientId to which to send the request based on the - * requestType. If requestRouter returns this connector's clientId, localRequestHandler is used - * @param localEventHandler Function that handles events from the server by accepting an eventType - * and an event and emitting the event locally - * @param networkConnectorEventHandlers Functions that run when network connector events occur - * like when clients are disconnected - * @returns Promise that resolves with connector info when finished connecting + * @returns Returns true if running on the extension host, false otherwise */ - connect: ( - localRequestHandler: InternalRequestHandler, - requestRouter: RequestRouter, - localEventHandler: InternalNetworkEventHandler, - networkConnectorEventHandlers: NetworkConnectorEventHandlers, - ) => Promise; + export const isExtensionHost: () => boolean; /** - * Notify the server that this client has received its connectorInfo and is ready to go. + * Gets which kind of process this is (main, renderer, extension-host) + * + * @returns ProcessType for this process + */ + export const getProcessType: () => ProcessType; +} +declare module "shared/data/network-connector.model" { + /** + * Types that are relevant particularly to the implementation of communication on + * NetworkConnector.ts files Do not use these types outside of ClientNetworkConnector.ts and + * ServerNetworkConnector.ts + */ + import { InternalEvent, InternalRequest, InternalResponse, NetworkConnectorInfo } from "shared/data/internal-connection.model"; + /** Port to use for the webSocket */ + export const WEBSOCKET_PORT = 8876; + /** Number of attempts a client will make to connect to the WebSocket server before failing */ + export const WEBSOCKET_ATTEMPTS_MAX = 5; + /** + * Time in ms for the client to wait before attempting to connect to the WebSocket server again + * after a failure + */ + export const WEBSOCKET_ATTEMPTS_WAIT = 1000; + /** WebSocket message type that indicates how to handle it */ + export enum MessageType { + InitClient = "init-client", + ClientConnect = "client-connect", + Request = "request", + Response = "response", + Event = "event" + } + /** Message sent to the client to give it NetworkConnectorInfo */ + export type InitClient = { + type: MessageType.InitClient; + senderId: number; + connectorInfo: NetworkConnectorInfo; + /** Guid unique to this connection. Used to verify important messages like reconnecting */ + clientGuid: string; + }; + /** Message responding to the server to let it know this connection is ready to receive messages */ + export type ClientConnect = { + type: MessageType.ClientConnect; + senderId: number; + /** + * ClientGuid for this client the last time it was connected to the server. Used when reconnecting + * (like if the browser refreshes): if the server has a connection with this clientGuid, it will + * unregister all requests on that client so the reconnecting client can register its request + * handlers again. + */ + reconnectingClientGuid?: string; + }; + /** Request to do something and to respond */ + export type WebSocketRequest = { + type: MessageType.Request; + /** What kind of request this is. Certain command, etc */ + requestType: string; + } & InternalRequest; + /** Response to a request */ + export type WebSocketResponse = { + type: MessageType.Response; + /** What kind of request this is. Certain command, etc */ + requestType: string; + } & InternalResponse; + /** Event to be sent out throughout all processes */ + export type WebSocketEvent = { + type: MessageType.Event; + /** What kind of event this is */ + eventType: string; + } & InternalEvent; + /** Messages send by the WebSocket */ + export type Message = InitClient | ClientConnect | WebSocketRequest | WebSocketResponse | WebSocketEvent; +} +declare module "shared/services/logger.service" { + import log from 'electron-log'; + export const WARN_TAG = ""; + /** + * Format a string of a service message + * + * @param message Message from the service + * @param serviceName Name of the service to show in the log + * @param tag Optional tag at the end of the service name + * @returns Formatted string of a service message + */ + export function formatLog(message: string, serviceName: string, tag?: string): string; + /** + * JSDOC SOURCE logger * - * MUST RUN AFTER connect() WHEN ITS PROMISE RESOLVES + * All extensions and services should use this logger to provide a unified output of logs + */ + const logger: log.MainLogger & { + default: log.MainLogger; + }; + export default logger; +} +declare module "client/services/web-socket.interface" { + /** + * Interface that defines the webSocket functionality the extension host and the renderer must + * implement. Used by WebSocketFactory to supply the right kind of WebSocket to + * ClientNetworkConnector. For now, we are just using the browser WebSocket type. We may need + * specific functionality that don't line up between the ws library's implementation and the browser + * implementation. We can adjust as needed at that point. + */ + export type IWebSocket = WebSocket; +} +declare module "renderer/services/renderer-web-socket.service" { + /** Once our network is running, run this to stop extensions from connecting to it directly */ + export const blockWebSocketsToPapiNetwork: () => void; + /** + * JSDOC SOURCE PapiRendererWebSocket This wraps the browser's WebSocket implementation to provide + * better control over internet access. It is isomorphic with the standard WebSocket, so it should + * act as a drop-in replacement. * - * TODO: Is this necessary? + * Note that the Node WebSocket implementation is different and not wrapped here. */ - notifyClientConnected: () => Promise; + export default class PapiRendererWebSocket implements WebSocket { + readonly CONNECTING: 0; + readonly OPEN: 1; + readonly CLOSING: 2; + readonly CLOSED: 3; + addEventListener: (type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions) => void; + binaryType: BinaryType; + bufferedAmount: number; + close: (code?: number, reason?: string) => void; + dispatchEvent: (event: Event) => boolean; + extensions: string; + onclose: ((this: WebSocket, ev: CloseEvent) => any) | null; + onerror: ((this: WebSocket, ev: Event) => any) | null; + onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null; + onopen: ((this: WebSocket, ev: Event) => any) | null; + protocol: string; + readyState: number; + removeEventListener: (type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | EventListenerOptions) => void; + send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void; + url: string; + constructor(url: string | URL, protocols?: string | string[]); + } +} +declare module "extension-host/services/extension-host-web-socket.model" { + import ws from 'ws'; + /** + * Extension-host client uses ws as its WebSocket client, but the renderer can't use it. So we need + * to exclude it from the renderer webpack bundle like this. + */ + export default ws; +} +declare module "client/services/web-socket.factory" { + import { IWebSocket } from "client/services/web-socket.interface"; + /** + * Creates a WebSocket for the renderer or extension host depending on where you're running + * + * @returns WebSocket + */ + export const createWebSocket: (url: string) => Promise; +} +declare module "client/services/client-network-connector.service" { + import { ConnectionStatus, InternalEvent, InternalNetworkEventHandler, InternalRequest, InternalRequestHandler, InternalResponse, NetworkConnectorInfo, RequestRouter } from "shared/data/internal-connection.model"; + import INetworkConnector from "shared/services/network-connector.interface"; + /** Handles the connection from the client to the server */ + export default class ClientNetworkConnector implements INetworkConnector { + connectorInfo: NetworkConnectorInfo; + connectionStatus: ConnectionStatus; + /** The webSocket connected to the server */ + private webSocket?; + /** + * All message subscriptions - emitters that emit an event each time a message with a specific + * message type comes in + */ + private messageEmitters; + /** + * Promise that resolves when the connection is finished or rejects if disconnected before the + * connection finishes + */ + private connectPromise?; + /** Function that removes this initClient handler from the connection */ + private unsubscribeHandleInitClientMessage?; + /** Function that removes this response handler from the connection */ + private unsubscribeHandleResponseMessage?; + /** Function that removes this handleRequest from the connection */ + private unsubscribeHandleRequestMessage?; + /** Function that removes this handleEvent from the connection */ + private unsubscribeHandleEventMessage?; + /** + * Function to call when we receive a request that is registered on this connector. Handles + * requests from the connection and returns a response to send back + */ + private localRequestHandler?; + /** + * Function to call when we are sending a request. Returns a clientId to which to send the request + * based on the requestType + */ + private requestRouter?; + /** + * Function to call when we receive an event. Handles events from the connection by emitting the + * event locally + */ + private localEventHandler?; + /** All requests that are waiting for a response */ + private requests; + /** Unique Guid associated with this connection. Used to verify certain things with server */ + private clientGuid; + connect: (localRequestHandler: InternalRequestHandler, requestRouter: RequestRouter, localEventHandler: InternalNetworkEventHandler) => Promise>; + notifyClientConnected: () => Promise; + disconnect: () => void; + request: (requestType: string, request: InternalRequest) => Promise>; + emitEventOnNetwork: (eventType: string, event: InternalEvent) => Promise; + /** + * Send a message to the server via webSocket. Throws if not connected + * + * @param message Message to send + */ + private sendMessage; + /** + * Receives and appropriately publishes server webSocket messages + * + * @param event WebSocket message information + * @param fromSelf Whether this message is from this connector instead of from someone else + */ + private onMessage; + /** + * Subscribes a function to run on webSocket messages of a particular type + * + * @param messageType The type of message on which to subscribe the function + * @param callback Function to run with the contents of the webSocket message + * @returns Unsubscriber function to run to stop calling the passed-in function on webSocket + * messages + */ + private subscribe; + /** + * Function that handles webSocket messages of type Response. Resolves the request associated with + * the received response message + * + * @param response Response message to resolve + */ + private handleResponseMessage; + /** + * Function that handles incoming webSocket messages and locally sent messages of type Request. + * Runs the requestHandler provided in connect() and sends a message with the response + * + * @param requestMessage Request message to handle + * @param isIncoming Whether this message is coming from the server and we should definitely + * handle it locally or if it is a locally sent request and we should send to the server if we + * don't have a local handler + */ + private handleRequestMessage; + /** + * Function that handles incoming webSocket messages of type Event. Runs the eventHandler provided + * in connect() + * + * @param eventMessage Event message to handle + */ + private handleEventMessage; + } +} +declare module "main/services/server-network-connector.service" { + import { ConnectionStatus, InternalEvent, InternalNetworkEventHandler, InternalRequest, InternalRequestHandler, InternalResponse, NetworkConnectorEventHandlers, NetworkConnectorInfo, RequestRouter } from "shared/data/internal-connection.model"; + import INetworkConnector from "shared/services/network-connector.interface"; + /** Handles the endpoint and connections from the server to the clients */ + export default class ServerNetworkConnector implements INetworkConnector { + connectorInfo: NetworkConnectorInfo; + connectionStatus: ConnectionStatus; + /** The webSocket connected to the server */ + private webSocketServer?; + /** The next client id to use for a new connection. Starts at 1 because the server is 0 */ + private nextClientId; + /** The webSocket clients that are connected and information about them */ + private clientSockets; + /** + * All message subscriptions - emitters that emit an event each time a message with a specific + * message type comes in + */ + private messageEmitters; + /** + * Promise that resolves when finished starting the server or rejects if disconnected before the + * server finishes + */ + private connectPromise?; + /** Function that removes this clientConnect handler from connections */ + private unsubscribeHandleClientConnectMessage?; + /** Function that removes this response handler from connections */ + private unsubscribeHandleResponseMessage?; + /** Function that removes this handleRequest from connections */ + private unsubscribeHandleRequestMessage?; + /** Function that removes this handleEvent from the connection */ + private unsubscribeHandleEventMessage?; + /** + * Function to call when we receive a request that is registered on this connector. Handles + * requests from connections and returns a response to send back + */ + private localRequestHandler?; + /** + * Function to call when we are sending a request. Returns a clientId to which to send the request + * based on the requestType + */ + private requestRouter?; + /** + * Function to call when we receive an event. Handles events from connections and emits the event + * locally + */ + private localEventHandler?; + /** Functions to run when network connector events occur like when clients are disconnected */ + private networkConnectorEventHandlers?; + /** All requests that are waiting for a response */ + private requests; + connect: (localRequestHandler: InternalRequestHandler, requestRouter: RequestRouter, localEventHandler: InternalNetworkEventHandler, networkConnectorEventHandlers: NetworkConnectorEventHandlers) => Promise>; + notifyClientConnected: () => Promise; + disconnect: () => void; + request: (requestType: string, request: InternalRequest) => Promise>; + emitEventOnNetwork: (eventType: string, event: InternalEvent) => Promise; + /** Get the client socket for a certain clientId. Throws if not found */ + private getClientSocket; + /** + * Attempts to get the client socket for a certain clientGuid. Returns undefined if not found. + * This does not throw because it will likely be very common that we do not have a clientId for a + * certain clientGuid as connecting clients will often supply old clientGuids. + */ + private getClientSocketFromGuid; + /** Get the clientId for a certain webSocket. Throws if not found */ + private getClientIdFromSocket; + /** + * Send a message to a client via webSocket. Throws if not connected + * + * @param message Message to send + * @param recipientId The client to which to send the message. TODO: determine if we can intuit + * this instead + */ + private sendMessage; + /** + * Receives and appropriately publishes webSocket messages + * + * @param event WebSocket message information + * @param fromSelf Whether this message is from this connector instead of from someone else + */ + private onMessage; + /** + * Subscribes a function to run on webSocket messages of a particular type + * + * @param messageType The type of message on which to subscribe the function + * @param callback Function to run with the contents of the webSocket message + * @returns Unsubscriber function to run to stop calling the passed-in function on webSocket + * messages + */ + private subscribe; + /** + * Registers an incoming webSocket connection and sends connection info with InitClient. Does not + * consider the client fully connected yet until they respond and tell us they connected with + * ClientConnect + */ + private onClientConnect; + /** Handles when client connection disconnects. Unregisters and such */ + private onClientDisconnect; + /** Closes connection and unregisters a client webSocket when it has disconnected */ + private disconnectClient; + /** + * Function that handles webSocket messages of type ClientConnect. Mark the connection fully + * connected and notify that a client connected or reconnected + * + * @param clientConnect Message from the client about the connection + * @param connectorId ClientId of the client who is sending this ClientConnect message + */ + private handleClientConnectMessage; + /** + * Function that handles webSocket messages of type Response. Resolves the request associated with + * the received response message or forwards to appropriate client + * + * @param response Response message to resolve + * @param responderId Responding client + */ + private handleResponseMessage; + /** + * Function that handles incoming webSocket messages and locally sent messages of type Request. + * Handles the request and sends a response if we have a handler or forwards to the appropriate + * client + * + * @param requestMessage Request to handle + * @param requesterId Who sent this message + */ + private handleRequestMessage; + /** + * Function that handles incoming webSocket messages of type Event. Runs the eventHandler provided + * in connect() and forwards the event to other clients + * + * @param eventMessage Event message to handle + */ + private handleEventMessage; + } +} +declare module "shared/services/network-connector.factory" { + import INetworkConnector from "shared/services/network-connector.interface"; /** - * Disconnects from the connection: + * Creates a NetworkConnector for the client or the server depending on where you're running * - * - On Client: disconnects from the server - * - On Server: disconnects from clients and closes its connection endpoint + * @returns NetworkConnector + */ + export const createNetworkConnector: () => Promise; +} +declare module "shared/services/connection.service" { + /** + * Handles setting up a connection to the electron backend and exchanging simple messages. Do not + * use outside NetworkService.ts. For communication, use NetworkService.ts as it is an abstraction + * over this. */ - disconnect: () => void; + import { NetworkConnectorEventHandlers, NetworkEventHandler, RequestHandler, RequestRouter } from "shared/data/internal-connection.model"; + import { ComplexResponse } from "shared/utils/util"; /** - * Send a request to the server/a client and resolve after receiving a response + * Send a request to the server and resolve after receiving a response * * @param requestType The type of request * @param contents Contents to send in the request * @returns Promise that resolves with the response message */ - request: InternalRequestHandler; + export const request: (requestType: string, contents: TParam) => Promise>; /** * Sends an event to other processes. Does NOT run the local event subscriptions as they should be * run by NetworkEventEmitter after sending on network. @@ -739,1251 +1023,582 @@ declare module 'shared/services/network-connector.interface' { * @param eventType Unique network event type for coordinating between processes * @param event Event to emit on the network */ - emitEventOnNetwork: (eventType: string, event: InternalEvent) => Promise; - } -} -declare module 'shared/utils/internal-util' { - /** Utility functions specific to the internal technologies we are using. */ - import { ProcessType } from 'shared/global-this.model'; - /** - * Determine if running on a client process (renderer, extension-host) or on the server. - * - * @returns Returns true if running on a client, false otherwise - */ - export const isClient: () => boolean; - /** - * Determine if running on the server process (main) - * - * @returns Returns true if running on the server, false otherwise - */ - export const isServer: () => boolean; - /** - * Determine if running on the renderer process - * - * @returns Returns true if running on the renderer, false otherwise - */ - export const isRenderer: () => boolean; - /** - * Determine if running on the extension host - * - * @returns Returns true if running on the extension host, false otherwise - */ - export const isExtensionHost: () => boolean; - /** - * Gets which kind of process this is (main, renderer, extension-host) - * - * @returns ProcessType for this process - */ - export const getProcessType: () => ProcessType; -} -declare module 'shared/data/network-connector.model' { - /** - * Types that are relevant particularly to the implementation of communication on - * NetworkConnector.ts files Do not use these types outside of ClientNetworkConnector.ts and - * ServerNetworkConnector.ts - */ - import { - InternalEvent, - InternalRequest, - InternalResponse, - NetworkConnectorInfo, - } from 'shared/data/internal-connection.model'; - /** Port to use for the webSocket */ - export const WEBSOCKET_PORT = 8876; - /** Number of attempts a client will make to connect to the WebSocket server before failing */ - export const WEBSOCKET_ATTEMPTS_MAX = 5; - /** - * Time in ms for the client to wait before attempting to connect to the WebSocket server again - * after a failure - */ - export const WEBSOCKET_ATTEMPTS_WAIT = 1000; - /** WebSocket message type that indicates how to handle it */ - export enum MessageType { - InitClient = 'init-client', - ClientConnect = 'client-connect', - Request = 'request', - Response = 'response', - Event = 'event', - } - /** Message sent to the client to give it NetworkConnectorInfo */ - export type InitClient = { - type: MessageType.InitClient; - senderId: number; - connectorInfo: NetworkConnectorInfo; - /** Guid unique to this connection. Used to verify important messages like reconnecting */ - clientGuid: string; - }; - /** Message responding to the server to let it know this connection is ready to receive messages */ - export type ClientConnect = { - type: MessageType.ClientConnect; - senderId: number; - /** - * ClientGuid for this client the last time it was connected to the server. Used when reconnecting - * (like if the browser refreshes): if the server has a connection with this clientGuid, it will - * unregister all requests on that client so the reconnecting client can register its request - * handlers again. - */ - reconnectingClientGuid?: string; - }; - /** Request to do something and to respond */ - export type WebSocketRequest = { - type: MessageType.Request; - /** What kind of request this is. Certain command, etc */ - requestType: string; - } & InternalRequest; - /** Response to a request */ - export type WebSocketResponse = { - type: MessageType.Response; - /** What kind of request this is. Certain command, etc */ - requestType: string; - } & InternalResponse; - /** Event to be sent out throughout all processes */ - export type WebSocketEvent = { - type: MessageType.Event; - /** What kind of event this is */ - eventType: string; - } & InternalEvent; - /** Messages send by the WebSocket */ - export type Message = - | InitClient - | ClientConnect - | WebSocketRequest - | WebSocketResponse - | WebSocketEvent; -} -declare module 'shared/services/logger.service' { - import log from 'electron-log'; - export const WARN_TAG = ''; - /** - * Format a string of a service message - * - * @param message Message from the service - * @param serviceName Name of the service to show in the log - * @param tag Optional tag at the end of the service name - * @returns Formatted string of a service message - */ - export function formatLog(message: string, serviceName: string, tag?: string): string; - /** - * - * All extensions and services should use this logger to provide a unified output of logs - */ - const logger: log.MainLogger & { - default: log.MainLogger; - }; - export default logger; -} -declare module 'client/services/web-socket.interface' { - /** - * Interface that defines the webSocket functionality the extension host and the renderer must - * implement. Used by WebSocketFactory to supply the right kind of WebSocket to - * ClientNetworkConnector. For now, we are just using the browser WebSocket type. We may need - * specific functionality that don't line up between the ws library's implementation and the browser - * implementation. We can adjust as needed at that point. - */ - export type IWebSocket = WebSocket; -} -declare module 'renderer/services/renderer-web-socket.service' { - /** Once our network is running, run this to stop extensions from connecting to it directly */ - export const blockWebSocketsToPapiNetwork: () => void; - /** This wraps the browser's WebSocket implementation to provide - * better control over internet access. It is isomorphic with the standard WebSocket, so it should - * act as a drop-in replacement. - * - * Note that the Node WebSocket implementation is different and not wrapped here. - */ - export default class PapiRendererWebSocket implements WebSocket { - readonly CONNECTING: 0; - readonly OPEN: 1; - readonly CLOSING: 2; - readonly CLOSED: 3; - addEventListener: ( - type: K, - listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, - options?: boolean | AddEventListenerOptions, - ) => void; - binaryType: BinaryType; - bufferedAmount: number; - close: (code?: number, reason?: string) => void; - dispatchEvent: (event: Event) => boolean; - extensions: string; - onclose: ((this: WebSocket, ev: CloseEvent) => any) | null; - onerror: ((this: WebSocket, ev: Event) => any) | null; - onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null; - onopen: ((this: WebSocket, ev: Event) => any) | null; - protocol: string; - readyState: number; - removeEventListener: ( - type: K, - listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, - options?: boolean | EventListenerOptions, - ) => void; - send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void; - url: string; - constructor(url: string | URL, protocols?: string | string[]); - } -} -declare module 'extension-host/services/extension-host-web-socket.model' { - import ws from 'ws'; - /** - * Extension-host client uses ws as its WebSocket client, but the renderer can't use it. So we need - * to exclude it from the renderer webpack bundle like this. - */ - export default ws; -} -declare module 'client/services/web-socket.factory' { - import { IWebSocket } from 'client/services/web-socket.interface'; - /** - * Creates a WebSocket for the renderer or extension host depending on where you're running - * - * @returns WebSocket - */ - export const createWebSocket: (url: string) => Promise; + export const emitEventOnNetwork: (eventType: string, event: T) => Promise; + /** Disconnects from the server */ + export const disconnect: () => void; + /** + * Sets up the ConnectionService by connecting to the server and setting up event handlers + * + * @param localRequestHandler Function that handles requests from the server by accepting a + * requestType and a ComplexRequest and returning a Promise of a Complex Response + * @param networkRequestRouter Function that determines the appropriate clientId to which to send + * requests of the given type + * @param localEventHandler Function that handles events from the server by accepting an eventType + * and an event and emitting the event locally + * @param connectorEventHandlers Functions that run when network connector events occur like when + * clients are disconnected + * @returns Promise that resolves when finished connecting + */ + export const connect: (localRequestHandler: RequestHandler, networkRequestRouter: RequestRouter, localEventHandler: NetworkEventHandler, connectorEventHandlers: NetworkConnectorEventHandlers) => Promise; + /** Gets this connection's clientId */ + export const getClientId: () => number; +} +declare module "shared/models/papi-network-event-emitter.model" { + import { PlatformEventHandler, PlatformEventEmitter } from 'platform-bible-utils'; + /** + * Networked version of EventEmitter - accepts subscriptions to an event and runs the subscription + * callbacks when the event is emitted. Events on NetworkEventEmitters can be emitted across + * processes. They are coordinated between processes by their type. Use eventEmitter.event(callback) + * to subscribe to the event. Use eventEmitter.emit(event) to run the subscriptions. Generally, this + * EventEmitter should be private, and its event should be public. That way, the emitter is not + * publicized, but anyone can subscribe to the event. + * + * WARNING: Do not use this class directly outside of NetworkService, or it will not do what you + * expect. Use NetworkService.createNetworkEventEmitter. + * + * WARNING: You cannot emit events with complex types on the network. + */ + export default class PapiNetworkEventEmitter extends PlatformEventEmitter { + /** Callback that sends the event to other processes on the network when it is emitted */ + private networkSubscriber; + /** Callback that runs when the emitter is disposed - should handle unlinking from the network */ + private networkDisposer; + /** + * Creates a NetworkEventEmitter + * + * @param networkSubscriber Callback that accepts the event and emits it to other processes + * @param networkDisposer Callback that unlinks this emitter from the network + */ + constructor( + /** Callback that sends the event to other processes on the network when it is emitted */ + networkSubscriber: PlatformEventHandler, + /** Callback that runs when the emitter is disposed - should handle unlinking from the network */ + networkDisposer: () => void); + emit: (event: T) => void; + /** + * Runs only the subscriptions for the event that are on this process. Does not send over network + * + * @param event Event data to provide to subscribed callbacks + */ + emitLocal(event: T): void; + dispose: () => Promise; + } } -declare module 'client/services/client-network-connector.service' { - import { - ConnectionStatus, - InternalEvent, - InternalNetworkEventHandler, - InternalRequest, - InternalRequestHandler, - InternalResponse, - NetworkConnectorInfo, - RequestRouter, - } from 'shared/data/internal-connection.model'; - import INetworkConnector from 'shared/services/network-connector.interface'; - /** Handles the connection from the client to the server */ - export default class ClientNetworkConnector implements INetworkConnector { - connectorInfo: NetworkConnectorInfo; - connectionStatus: ConnectionStatus; - /** The webSocket connected to the server */ - private webSocket?; - /** - * All message subscriptions - emitters that emit an event each time a message with a specific - * message type comes in - */ - private messageEmitters; - /** - * Promise that resolves when the connection is finished or rejects if disconnected before the - * connection finishes - */ - private connectPromise?; - /** Function that removes this initClient handler from the connection */ - private unsubscribeHandleInitClientMessage?; - /** Function that removes this response handler from the connection */ - private unsubscribeHandleResponseMessage?; - /** Function that removes this handleRequest from the connection */ - private unsubscribeHandleRequestMessage?; - /** Function that removes this handleEvent from the connection */ - private unsubscribeHandleEventMessage?; - /** - * Function to call when we receive a request that is registered on this connector. Handles - * requests from the connection and returns a response to send back - */ - private localRequestHandler?; - /** - * Function to call when we are sending a request. Returns a clientId to which to send the request - * based on the requestType - */ - private requestRouter?; - /** - * Function to call when we receive an event. Handles events from the connection by emitting the - * event locally - */ - private localEventHandler?; - /** All requests that are waiting for a response */ - private requests; - /** Unique Guid associated with this connection. Used to verify certain things with server */ - private clientGuid; - connect: ( - localRequestHandler: InternalRequestHandler, - requestRouter: RequestRouter, - localEventHandler: InternalNetworkEventHandler, - ) => Promise< - Readonly<{ - clientId: number; - }> - >; - notifyClientConnected: () => Promise; - disconnect: () => void; - request: ( - requestType: string, - request: InternalRequest, - ) => Promise>; - emitEventOnNetwork: (eventType: string, event: InternalEvent) => Promise; +declare module "shared/services/network.service" { + /** + * Handles requests, responses, subscriptions, etc. to the backend. Likely shouldn't need/want to + * expose this whole service on papi, but there are a few things that are exposed via + * papiNetworkService + */ + import { ClientConnectEvent, ClientDisconnectEvent } from "shared/data/internal-connection.model"; + import { UnsubscriberAsync, PlatformEventEmitter, PlatformEvent } from 'platform-bible-utils'; + import { ComplexRequest, ComplexResponse, RequestHandlerType, SerializedRequestType } from "shared/utils/util"; + /** + * Args handler function for a request. Called when a request is handled. The function should accept + * the spread of the contents array of the request as its parameters. The function should return an + * object that becomes the contents object of the response. This type of handler is a normal + * function. + */ + type ArgsRequestHandler = any[], TReturn = any> = (...args: TParam) => Promise | TReturn; + /** + * Contents handler function for a request. Called when a request is handled. The function should + * accept the contents object of the request as its single parameter. The function should return an + * object that becomes the contents object of the response. + */ + type ContentsRequestHandler = (contents: TParam) => Promise; + /** + * Complex handler function for a request. Called when a request is handled. The function should + * accept a ComplexRequest object as its single parameter. The function should return a + * ComplexResponse object that becomes the response.. This type of handler is the most flexible of + * the request handlers. + */ + type ComplexRequestHandler = (request: ComplexRequest) => Promise>; + /** Event that emits with clientId when a client connects */ + export const onDidClientConnect: PlatformEvent; + /** Event that emits with clientId when a client disconnects */ + export const onDidClientDisconnect: PlatformEvent; + /** Closes the network services gracefully */ + export const shutdown: () => void; + /** Sets up the NetworkService. Runs only once */ + export const initialize: () => Promise; /** - * Send a message to the server via webSocket. Throws if not connected + * Send a request on the network and resolve the response contents. * - * @param message Message to send + * @param requestType The type of request + * @param args Arguments to send in the request (put in request.contents) + * @returns Promise that resolves with the response message */ - private sendMessage; + export const request: (requestType: SerializedRequestType, ...args: TParam) => Promise; /** - * Receives and appropriately publishes server webSocket messages + * Register a local request handler to run on requests. * - * @param event WebSocket message information - * @param fromSelf Whether this message is from this connector instead of from someone else + * @param requestType The type of request on which to register the handler + * @param handler Function to register to run on requests + * @param handlerType Type of handler function - indicates what type of parameters and what return + * type the handler has + * @returns Promise that resolves if the request successfully registered and unsubscriber function + * to run to stop the passed-in function from handling requests */ - private onMessage; + export function registerRequestHandler(requestType: SerializedRequestType, handler: ArgsRequestHandler, handlerType?: RequestHandlerType): Promise; + export function registerRequestHandler(requestType: SerializedRequestType, handler: ContentsRequestHandler, handlerType?: RequestHandlerType): Promise; + export function registerRequestHandler(requestType: SerializedRequestType, handler: ComplexRequestHandler, handlerType?: RequestHandlerType): Promise; /** - * Subscribes a function to run on webSocket messages of a particular type + * Creates an event emitter that works properly over the network. Other connections receive this + * event when it is emitted. + * + * WARNING: You can only create a network event emitter once per eventType to prevent hijacked event + * emitters. * - * @param messageType The type of message on which to subscribe the function - * @param callback Function to run with the contents of the webSocket message - * @returns Unsubscriber function to run to stop calling the passed-in function on webSocket - * messages + * WARNING: You cannot emit events with complex types on the network. + * + * @param eventType Unique network event type for coordinating between connections + * @returns Event emitter whose event works between connections */ - private subscribe; + export const createNetworkEventEmitter: (eventType: string) => PlatformEventEmitter; /** - * Function that handles webSocket messages of type Response. Resolves the request associated with - * the received response message + * Gets the network event with the specified type. Creates the emitter if it does not exist * - * @param response Response message to resolve + * @param eventType Unique network event type for coordinating between connections + * @returns Event for the event type that runs the callback provided when the event is emitted */ - private handleResponseMessage; + export const getNetworkEvent: (eventType: string) => PlatformEvent; /** - * Function that handles incoming webSocket messages and locally sent messages of type Request. - * Runs the requestHandler provided in connect() and sends a message with the response + * Creates a function that is a request function with a baked requestType. This is also nice because + * you get TypeScript type support using this function. * - * @param requestMessage Request message to handle - * @param isIncoming Whether this message is coming from the server and we should definitely - * handle it locally or if it is a locally sent request and we should send to the server if we - * don't have a local handler + * @param requestType RequestType for request function + * @returns Function to call with arguments of request that performs the request and resolves with + * the response contents */ - private handleRequestMessage; + export const createRequestFunction: (requestType: SerializedRequestType) => (...args: TParam) => Promise; + export interface PapiNetworkService { + onDidClientConnect: typeof onDidClientConnect; + onDidClientDisconnect: typeof onDidClientDisconnect; + createNetworkEventEmitter: typeof createNetworkEventEmitter; + getNetworkEvent: typeof getNetworkEvent; + } /** - * Function that handles incoming webSocket messages of type Event. Runs the eventHandler provided - * in connect() + * JSDOC SOURCE papiNetworkService * - * @param eventMessage Event message to handle + * Service that provides a way to send and receive network events */ - private handleEventMessage; - } + export const papiNetworkService: PapiNetworkService; } -declare module 'main/services/server-network-connector.service' { - import { - ConnectionStatus, - InternalEvent, - InternalNetworkEventHandler, - InternalRequest, - InternalRequestHandler, - InternalResponse, - NetworkConnectorEventHandlers, - NetworkConnectorInfo, - RequestRouter, - } from 'shared/data/internal-connection.model'; - import INetworkConnector from 'shared/services/network-connector.interface'; - /** Handles the endpoint and connections from the server to the clients */ - export default class ServerNetworkConnector implements INetworkConnector { - connectorInfo: NetworkConnectorInfo; - connectionStatus: ConnectionStatus; - /** The webSocket connected to the server */ - private webSocketServer?; - /** The next client id to use for a new connection. Starts at 1 because the server is 0 */ - private nextClientId; - /** The webSocket clients that are connected and information about them */ - private clientSockets; - /** - * All message subscriptions - emitters that emit an event each time a message with a specific - * message type comes in - */ - private messageEmitters; - /** - * Promise that resolves when finished starting the server or rejects if disconnected before the - * server finishes - */ - private connectPromise?; - /** Function that removes this clientConnect handler from connections */ - private unsubscribeHandleClientConnectMessage?; - /** Function that removes this response handler from connections */ - private unsubscribeHandleResponseMessage?; - /** Function that removes this handleRequest from connections */ - private unsubscribeHandleRequestMessage?; - /** Function that removes this handleEvent from the connection */ - private unsubscribeHandleEventMessage?; - /** - * Function to call when we receive a request that is registered on this connector. Handles - * requests from connections and returns a response to send back - */ - private localRequestHandler?; - /** - * Function to call when we are sending a request. Returns a clientId to which to send the request - * based on the requestType - */ - private requestRouter?; - /** - * Function to call when we receive an event. Handles events from connections and emits the event - * locally - */ - private localEventHandler?; - /** Functions to run when network connector events occur like when clients are disconnected */ - private networkConnectorEventHandlers?; - /** All requests that are waiting for a response */ - private requests; - connect: ( - localRequestHandler: InternalRequestHandler, - requestRouter: RequestRouter, - localEventHandler: InternalNetworkEventHandler, - networkConnectorEventHandlers: NetworkConnectorEventHandlers, - ) => Promise< - Readonly<{ - clientId: number; - }> - >; - notifyClientConnected: () => Promise; - disconnect: () => void; - request: ( - requestType: string, - request: InternalRequest, - ) => Promise>; - emitEventOnNetwork: (eventType: string, event: InternalEvent) => Promise; - /** Get the client socket for a certain clientId. Throws if not found */ - private getClientSocket; +declare module "shared/services/network-object.service" { + import { PlatformEvent, UnsubscriberAsync } from 'platform-bible-utils'; + import { NetworkObject, DisposableNetworkObject, NetworkableObject, LocalObjectToProxyCreator, NetworkObjectDetails } from "shared/models/network-object.model"; + /** Sets up the service. Only runs once and always returns the same promise after that */ + const initialize: () => Promise; + /** + * Search locally known network objects for the given ID. Don't look on the network for more + * objects. + * + * @returns Whether we know of an existing network object with the provided ID already on the + * network + */ + const hasKnown: (id: string) => boolean; /** - * Attempts to get the client socket for a certain clientGuid. Returns undefined if not found. - * This does not throw because it will likely be very common that we do not have a clientId for a - * certain clientGuid as connecting clients will often supply old clientGuids. + * Event that fires when a new object has been created on the network (locally or remotely). The + * event contains information about the new network object. */ - private getClientSocketFromGuid; - /** Get the clientId for a certain webSocket. Throws if not found */ - private getClientIdFromSocket; + export const onDidCreateNetworkObject: PlatformEvent; + /** Event that fires with a network object ID when that object is disposed locally or remotely */ + export const onDidDisposeNetworkObject: PlatformEvent; + interface IDisposableObject { + dispose?: UnsubscriberAsync; + } + /** If `dispose` already exists on `objectToMutate`, we will call it in addition to `newDispose` */ + export function overrideDispose(objectToMutate: IDisposableObject, newDispose: UnsubscriberAsync): void; + /** + * Get a network object that has previously been set up to be shared on the network. A network + * object is a proxy to an object living somewhere else that local code can use. + * + * Running this function twice with the same inputs yields the same network object. + * + * @param id ID of the network object - all processes must use this ID to look up this network + * object + * @param createLocalObjectToProxy Function that creates an object that the network object proxy + * will be based upon. The object this function creates cannot have an `onDidDispose` property. + * This function is useful for setting up network events on a network object. + * @returns A promise for the network object with specified ID if one exists, undefined otherwise + */ + const get: (id: string, createLocalObjectToProxy?: LocalObjectToProxyCreator | undefined) => Promise | undefined>; + /** + * Set up an object to be shared on the network. + * + * @param id ID of the object to share on the network. All processes must use this ID to look it up. + * @param objectToShare The object to set up as a network object. It will have an event named + * `onDidDispose` added to its properties. An error will be thrown if the object already had an + * `onDidDispose` property on it. If the object already contained a `dispose` function, a new + * `dispose` function will be set that calls the existing function (amongst other things). If the + * object did not already define a `dispose` function, one will be added. + * + * WARNING: setting a network object mutates the provided object. + * @returns `objectToShare` modified to be a network object + */ + const set: (id: string, objectToShare: T, objectType?: string, objectAttributes?: { + [property: string]: unknown; + } | undefined) => Promise>; + interface NetworkObjectService { + initialize: typeof initialize; + hasKnown: typeof hasKnown; + get: typeof get; + set: typeof set; + onDidCreateNetworkObject: typeof onDidCreateNetworkObject; + } + /** + * Network objects are distributed objects within PAPI for TS/JS objects. @see + * https://en.wikipedia.org/wiki/Distributed_object + * + * Objects registered via {@link networkObjectService.set} are retrievable using + * {@link networkObjectService.get}. + * + * Function calls made on network objects retrieved via {@link networkObjectService.get} are proxied + * and sent to the original objects registered via {@link networkObjectService.set}. All functions on + * the registered object are proxied except for constructors, `dispose`, and functions starting with + * `on` since those should be events (which are not intended to be proxied) based on our naming + * convention. If you don't want a function to be proxied, don't make it a property of the + * registered object. + * + * Functions on a network object will be called asynchronously by other processes regardless of + * whether the functions are synchronous or asynchronous, so it is best to make them all + * asynchronous. All shared functions' arguments and return values must be serializable to be called + * across processes. + * + * When a service registers an object via {@link networkObjectService.set}, it is the responsibility + * of that service, and only that service, to call `dispose` on that object when it is no longer + * intended to be shared with other services. + * + * When an object is disposed by calling `dispose`, all functions registered with the `onDidDispose` + * event handler will be called. After an object is disposed, calls to its functions will no longer + * be proxied to the original object. + */ + const networkObjectService: NetworkObjectService; + export default networkObjectService; +} +declare module "shared/models/network-object.model" { + import { Dispose, OnDidDispose, CannotHaveOnDidDispose, CanHaveOnDidDispose } from 'platform-bible-utils'; + /** + * An object of this type is returned from {@link networkObjectService.get}. + * + * Override the NetworkableObject type's force-undefined onDidDispose to NetworkObject's + * onDidDispose type because it will have an onDidDispose added. + * + * If an object of type T had `dispose` on it, `networkObjectService.get` will remove the ability to + * 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 + */ + export type NetworkObject = Omit, 'dispose'> & OnDidDispose; + /** + * An object of this type is returned from {@link networkObjectService.set}. + * + * @see networkObjectService + */ + export type DisposableNetworkObject = NetworkObject & Dispose; + /** + * An object of this type is passed into {@link networkObjectService.set}. + * + * @see networkObjectService + */ + export type NetworkableObject = T & CannotHaveOnDidDispose; + /** + * If a network object with the provided ID exists remotely but has not been set up to use inside + * this process, this function is run in {@link networkObjectService.get}, and the returned object is + * used as a base on which to set up a NetworkObject for use on this process. All properties that + * are exposed in the base object will be used as-is, and all other properties will be assumed to + * exist on the remote network object. + * + * @param id ID of the network object to get + * @param networkObjectContainer Holds a reference to the NetworkObject that will be setup within + * {@link networkObjectService.get}. It is passed in to allow the return value to call functions on + * the NetworkObject. NOTE: networkObjectContainer.contents does not point to a real NetworkObject + * while this function is running. The real reference is assigned later, but before the + * NetworkObject will be used. The return value should always reference the NetworkObject as + * `networkObjectContainer.contents` to avoid acting upon an undefined NetworkObject. + * @returns The local object to proxy into a network object. + * + * 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 + */ + export type LocalObjectToProxyCreator = (id: string, networkObjectPromise: Promise>) => Partial; + /** Data about an object shared on the network */ + export type NetworkObjectDetails = { + /** ID of the network object that processes use to reference it */ + id: string; + /** + * Name of the type of this network object. Note this isn't about TypeScript types, but instead + * focused on the platform data model. Names of types for the same logical thing (e.g., Project + * Data Providers => `pdp`) should be the same across all process on the network regardless of + * what programming language they use. For generic network objects, `networkObject` is + * appropriate. + */ + objectType: string; + /** Array of strings with the function names exposed on this network object */ + functionNames: string[]; + /** + * Optional object containing properties that describe this network object. The properties + * associated with this network object depend on the `objectType`. + */ + attributes?: Record; + }; +} +declare module "shared/models/data-provider.model" { + import { UnsubscriberAsync, PlatformEventHandler } from 'platform-bible-utils'; + import { NetworkableObject } from "shared/models/network-object.model"; + /** Various options to adjust how the data provider subscriber emits updates */ + export type DataProviderSubscriberOptions = { + /** + * Whether to immediately retrieve the data for this subscriber and run the callback as soon as + * possible. + * + * This allows a subscriber to simply subscribe and provide a callback instead of subscribing, + * running `get`, and managing the race condition of an event coming in to update the data and the + * initial `get` coming back in. + * + * @default true + */ + retrieveDataImmediately?: boolean; + /** + * Under which conditions to run the callback when we receive updates to the data. + * + * - `'deeply-equal'` - only run the update callback when the data at this selector has changed. + * + * For example, suppose your selector is targeting John 3:5, and the data provider updates its + * data for Luke 5:3. Your data at John 3:5 does not change, and your callback will not run. + * - `'*'` - run the update callback every time the data has been updated whether or not the data at + * this selector has changed. + * + * For example, suppose your selector is targeting John 3:5, and the data provider updates its + * data for Luke 5:3. Your data at John 3:5 does not change, but your callback will run again + * with the same data anyway. + * + * @default 'deeply-equal' + */ + whichUpdates?: 'deeply-equal' | '*'; + }; /** - * Send a message to a client via webSocket. Throws if not connected + * Information that papi uses to interpret whether to send out updates on a data provider when the + * engine runs `set` or `notifyUpdate`. + * + * - `'*'` update subscriptions for all data types on this data provider + * - `string` name of data type - update subscriptions for this data type + * - `string[]` names of data types - update subscriptions for the data types in the array + * - `true` (or other truthy values other than strings and arrays) * - * @param message Message to send - * @param recipientId The client to which to send the message. TODO: determine if we can intuit - * this instead + * - In `set` - update subscriptions for this data type + * - In `notifyUpdate` - same as '*' + * - `false` (or falsy) do not update subscriptions */ - private sendMessage; + export type DataProviderUpdateInstructions = '*' | DataTypeNames | DataTypeNames[] | boolean; /** - * Receives and appropriately publishes webSocket messages + * Set a subset of data according to the selector. * - * @param event WebSocket message information - * @param fromSelf Whether this message is from this connector instead of from someone else + * Note: if a data provider engine does not provide `set` (possibly indicating it is read-only), + * this will throw an exception. + * + * @param selector Tells the provider what subset of data is being set + * @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 */ - private onMessage; + export type DataProviderSetter = (selector: TDataTypes[DataType]['selector'], data: TDataTypes[DataType]['setData']) => Promise>; /** - * Subscribes a function to run on webSocket messages of a particular type + * Get a subset of data from the provider according to the selector. + * + * Note: This is good for retrieving data from a provider once. If you want to keep the data + * up-to-date, use `subscribe` instead, which can immediately give you the data and keep it + * up-to-date. * - * @param messageType The type of message on which to subscribe the function - * @param callback Function to run with the contents of the webSocket message - * @returns Unsubscriber function to run to stop calling the passed-in function on webSocket - * messages + * @param selector Tells the provider what subset of data to get + * @returns The subset of data represented by the selector */ - private subscribe; + export type DataProviderGetter = (selector: TDataType['selector']) => Promise; + /** + * Subscribe to receive updates relevant to the provided selector from this data provider for a + * specific data type. + * + * Note: By default, this `subscribe` function automatically retrieves the current state + * of the data 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 `get`. You can turn this + * functionality off in the `options` parameter. + * + * @param selector Tells the provider what data this listener is listening for + * @param callback Function to run with the updated data for this selector + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber to stop listening for updates + */ + export type DataProviderSubscriber = (selector: TDataType['selector'], callback: PlatformEventHandler, options?: DataProviderSubscriberOptions) => Promise; + /** + * A helper type describing the types associated with a data provider's methods for a specific data + * type it handles. + * + * @type `TSelector` - The type of selector used to get some data from this provider at this data + * type. A selector is an object a caller provides to the data provider to tell the provider what + * subset of data it wants at this data type. + * @type `TGetData` - The type of data provided by this data provider when you run `get` + * based on a provided selector + * @type `TSetData` - The type of data ingested by this data provider when you run `set` + * based on a provided selector + */ + export type DataProviderDataType = { + /** + * The type of selector used to get some data from this provider at this data type. A selector is + * an object a caller provides to the data provider to tell the provider what subset of data it + * wants at this data type. + */ + selector: TSelector; + /** + * The type of data provided by this data provider when you run `get` based on a + * provided selector + */ + getData: TGetData; + /** + * The type of data ingested by this data provider when you run `set` based on a + * provided selector + */ + setData: TSetData; + }; /** - * Registers an incoming webSocket connection and sends connection info with InitClient. Does not - * consider the client fully connected yet until they respond and tell us they connected with - * ClientConnect + * A helper type describing all the data types a data provider handles. Each property on this type + * (consisting of a DataProviderDataType, which describes the types that correspond to that data + * type) describes a data type that the data provider handles. The data provider has a + * `set`, `get`, and `subscribe` for each property (aka data type) + * listed in this type. + * + * @example A data provider that handles greeting strings and age numbers (as well as an All data + * type that just provides all the data) could have a DataProviderDataTypes that looks like the + * following: + * + * ```typescript + * { + * Greeting: DataProviderDataType; + * Age: DataProviderDataType; + * All: DataProviderDataType; + * } + * ``` */ - private onClientConnect; - /** Handles when client connection disconnects. Unregisters and such */ - private onClientDisconnect; - /** Closes connection and unregisters a client webSocket when it has disconnected */ - private disconnectClient; + export type DataProviderDataTypes = { + [dataType: string]: DataProviderDataType; + }; /** - * Function that handles webSocket messages of type ClientConnect. Mark the connection fully - * connected and notify that a client connected or reconnected + * 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`) * - * @param clientConnect Message from the client about the connection - * @param connectorId ClientId of the client who is sending this ClientConnect message + * @see DataProviderDataTypes for more information */ - private handleClientConnectMessage; + export type DataTypeNames = keyof TDataTypes & string; /** - * Function that handles webSocket messages of type Response. Resolves the request associated with - * the received response message or forwards to appropriate client + * Set of all `set` methods that a data provider provides according to its data types. * - * @param response Response message to resolve - * @param responderId Responding client + * @see DataProviderSetter for more information */ - private handleResponseMessage; + export type DataProviderSetters = { + [DataType in keyof TDataTypes as `set${DataType & string}`]: DataProviderSetter; + }; /** - * Function that handles incoming webSocket messages and locally sent messages of type Request. - * Handles the request and sends a response if we have a handler or forwards to the appropriate - * client + * Set of all `get` methods that a data provider provides according to its data types. * - * @param requestMessage Request to handle - * @param requesterId Who sent this message + * @see DataProviderGetter for more information */ - private handleRequestMessage; + export type DataProviderGetters = { + [DataType in keyof TDataTypes as `get${DataType & string}`]: DataProviderGetter; + }; /** - * Function that handles incoming webSocket messages of type Event. Runs the eventHandler provided - * in connect() and forwards the event to other clients + * Set of all `subscribe` methods that a data provider provides according to its data + * types. * - * @param eventMessage Event message to handle + * @see DataProviderSubscriber for more information */ - private handleEventMessage; - } -} -declare module 'shared/services/network-connector.factory' { - import INetworkConnector from 'shared/services/network-connector.interface'; - /** - * Creates a NetworkConnector for the client or the server depending on where you're running - * - * @returns NetworkConnector - */ - export const createNetworkConnector: () => Promise; -} -declare module 'shared/services/connection.service' { - /** - * Handles setting up a connection to the electron backend and exchanging simple messages. Do not - * use outside NetworkService.ts. For communication, use NetworkService.ts as it is an abstraction - * over this. - */ - import { - NetworkConnectorEventHandlers, - NetworkEventHandler, - RequestHandler, - RequestRouter, - } from 'shared/data/internal-connection.model'; - import { ComplexResponse } from 'shared/utils/util'; - /** - * Send a request to the server and resolve after receiving a response - * - * @param requestType The type of request - * @param contents Contents to send in the request - * @returns Promise that resolves with the response message - */ - export const request: ( - requestType: string, - contents: TParam, - ) => Promise>; - /** - * Sends an event to other processes. Does NOT run the local event subscriptions as they should be - * run by NetworkEventEmitter after sending on network. - * - * @param eventType Unique network event type for coordinating between processes - * @param event Event to emit on the network - */ - export const emitEventOnNetwork: (eventType: string, event: T) => Promise; - /** Disconnects from the server */ - export const disconnect: () => void; - /** - * Sets up the ConnectionService by connecting to the server and setting up event handlers - * - * @param localRequestHandler Function that handles requests from the server by accepting a - * requestType and a ComplexRequest and returning a Promise of a Complex Response - * @param networkRequestRouter Function that determines the appropriate clientId to which to send - * requests of the given type - * @param localEventHandler Function that handles events from the server by accepting an eventType - * and an event and emitting the event locally - * @param connectorEventHandlers Functions that run when network connector events occur like when - * clients are disconnected - * @returns Promise that resolves when finished connecting - */ - export const connect: ( - localRequestHandler: RequestHandler, - networkRequestRouter: RequestRouter, - localEventHandler: NetworkEventHandler, - connectorEventHandlers: NetworkConnectorEventHandlers, - ) => Promise; - /** Gets this connection's clientId */ - export const getClientId: () => number; -} -declare module 'shared/models/papi-network-event-emitter.model' { - import { PlatformEventHandler, PlatformEventEmitter } from 'platform-bible-utils'; - /** - * Networked version of EventEmitter - accepts subscriptions to an event and runs the subscription - * callbacks when the event is emitted. Events on NetworkEventEmitters can be emitted across - * processes. They are coordinated between processes by their type. Use eventEmitter.event(callback) - * to subscribe to the event. Use eventEmitter.emit(event) to run the subscriptions. Generally, this - * EventEmitter should be private, and its event should be public. That way, the emitter is not - * publicized, but anyone can subscribe to the event. - * - * WARNING: Do not use this class directly outside of NetworkService, or it will not do what you - * expect. Use NetworkService.createNetworkEventEmitter. - * - * WARNING: You cannot emit events with complex types on the network. - */ - export default class PapiNetworkEventEmitter extends PlatformEventEmitter { - /** Callback that sends the event to other processes on the network when it is emitted */ - private networkSubscriber; - /** Callback that runs when the emitter is disposed - should handle unlinking from the network */ - private networkDisposer; - /** - * Creates a NetworkEventEmitter - * - * @param networkSubscriber Callback that accepts the event and emits it to other processes - * @param networkDisposer Callback that unlinks this emitter from the network - */ - constructor( - /** Callback that sends the event to other processes on the network when it is emitted */ - networkSubscriber: PlatformEventHandler, - /** Callback that runs when the emitter is disposed - should handle unlinking from the network */ - networkDisposer: () => void, - ); - emit: (event: T) => void; - /** - * Runs only the subscriptions for the event that are on this process. Does not send over network - * - * @param event Event data to provide to subscribed callbacks - */ - emitLocal(event: T): void; - dispose: () => Promise; - } -} -declare module 'shared/services/network.service' { - /** - * Handles requests, responses, subscriptions, etc. to the backend. Likely shouldn't need/want to - * expose this whole service on papi, but there are a few things that are exposed via - * papiNetworkService - */ - import { ClientConnectEvent, ClientDisconnectEvent } from 'shared/data/internal-connection.model'; - import { UnsubscriberAsync, PlatformEventEmitter, PlatformEvent } from 'platform-bible-utils'; - import { - ComplexRequest, - ComplexResponse, - RequestHandlerType, - SerializedRequestType, - } from 'shared/utils/util'; - /** - * Args handler function for a request. Called when a request is handled. The function should accept - * the spread of the contents array of the request as its parameters. The function should return an - * object that becomes the contents object of the response. This type of handler is a normal - * function. - */ - type ArgsRequestHandler = any[], TReturn = any> = ( - ...args: TParam - ) => Promise | TReturn; - /** - * Contents handler function for a request. Called when a request is handled. The function should - * accept the contents object of the request as its single parameter. The function should return an - * object that becomes the contents object of the response. - */ - type ContentsRequestHandler = (contents: TParam) => Promise; - /** - * Complex handler function for a request. Called when a request is handled. The function should - * accept a ComplexRequest object as its single parameter. The function should return a - * ComplexResponse object that becomes the response.. This type of handler is the most flexible of - * the request handlers. - */ - type ComplexRequestHandler = ( - request: ComplexRequest, - ) => Promise>; - /** Event that emits with clientId when a client connects */ - export const onDidClientConnect: PlatformEvent; - /** Event that emits with clientId when a client disconnects */ - export const onDidClientDisconnect: PlatformEvent; - /** Closes the network services gracefully */ - export const shutdown: () => void; - /** Sets up the NetworkService. Runs only once */ - export const initialize: () => Promise; - /** - * Send a request on the network and resolve the response contents. - * - * @param requestType The type of request - * @param args Arguments to send in the request (put in request.contents) - * @returns Promise that resolves with the response message - */ - export const request: ( - requestType: SerializedRequestType, - ...args: TParam - ) => Promise; - /** - * Register a local request handler to run on requests. - * - * @param requestType The type of request on which to register the handler - * @param handler Function to register to run on requests - * @param handlerType Type of handler function - indicates what type of parameters and what return - * type the handler has - * @returns Promise that resolves if the request successfully registered and unsubscriber function - * to run to stop the passed-in function from handling requests - */ - export function registerRequestHandler( - requestType: SerializedRequestType, - handler: ArgsRequestHandler, - handlerType?: RequestHandlerType, - ): Promise; - export function registerRequestHandler( - requestType: SerializedRequestType, - handler: ContentsRequestHandler, - handlerType?: RequestHandlerType, - ): Promise; - export function registerRequestHandler( - requestType: SerializedRequestType, - handler: ComplexRequestHandler, - handlerType?: RequestHandlerType, - ): Promise; - /** - * Creates an event emitter that works properly over the network. Other connections receive this - * event when it is emitted. - * - * WARNING: You can only create a network event emitter once per eventType to prevent hijacked event - * emitters. - * - * WARNING: You cannot emit events with complex types on the network. - * - * @param eventType Unique network event type for coordinating between connections - * @returns Event emitter whose event works between connections - */ - export const createNetworkEventEmitter: (eventType: string) => PlatformEventEmitter; - /** - * Gets the network event with the specified type. Creates the emitter if it does not exist - * - * @param eventType Unique network event type for coordinating between connections - * @returns Event for the event type that runs the callback provided when the event is emitted - */ - export const getNetworkEvent: (eventType: string) => PlatformEvent; - /** - * Creates a function that is a request function with a baked requestType. This is also nice because - * you get TypeScript type support using this function. - * - * @param requestType RequestType for request function - * @returns Function to call with arguments of request that performs the request and resolves with - * the response contents - */ - export const createRequestFunction: ( - requestType: SerializedRequestType, - ) => (...args: TParam) => Promise; - export interface PapiNetworkService { - onDidClientConnect: typeof onDidClientConnect; - onDidClientDisconnect: typeof onDidClientDisconnect; - createNetworkEventEmitter: typeof createNetworkEventEmitter; - getNetworkEvent: typeof getNetworkEvent; - } - /** - * - * Service that provides a way to send and receive network events - */ - export const papiNetworkService: PapiNetworkService; -} -declare module 'shared/services/network-object.service' { - import { PlatformEvent, UnsubscriberAsync } from 'platform-bible-utils'; - import { - NetworkObject, - DisposableNetworkObject, - NetworkableObject, - LocalObjectToProxyCreator, - NetworkObjectDetails, - } from 'shared/models/network-object.model'; - /** Sets up the service. Only runs once and always returns the same promise after that */ - const initialize: () => Promise; - /** - * Search locally known network objects for the given ID. Don't look on the network for more - * objects. - * - * @returns Whether we know of an existing network object with the provided ID already on the - * network - */ - const hasKnown: (id: string) => boolean; - /** - * Event that fires when a new object has been created on the network (locally or remotely). The - * event contains information about the new network object. - */ - export const onDidCreateNetworkObject: PlatformEvent; - /** Event that fires with a network object ID when that object is disposed locally or remotely */ - export const onDidDisposeNetworkObject: PlatformEvent; - interface IDisposableObject { - dispose?: UnsubscriberAsync; - } - /** If `dispose` already exists on `objectToMutate`, we will call it in addition to `newDispose` */ - export function overrideDispose( - objectToMutate: IDisposableObject, - newDispose: UnsubscriberAsync, - ): void; - /** - * Get a network object that has previously been set up to be shared on the network. A network - * object is a proxy to an object living somewhere else that local code can use. - * - * Running this function twice with the same inputs yields the same network object. - * - * @param id ID of the network object - all processes must use this ID to look up this network - * object - * @param createLocalObjectToProxy Function that creates an object that the network object proxy - * will be based upon. The object this function creates cannot have an `onDidDispose` property. - * This function is useful for setting up network events on a network object. - * @returns A promise for the network object with specified ID if one exists, undefined otherwise - */ - const get: ( - id: string, - createLocalObjectToProxy?: LocalObjectToProxyCreator | undefined, - ) => Promise | undefined>; - /** - * Set up an object to be shared on the network. - * - * @param id ID of the object to share on the network. All processes must use this ID to look it up. - * @param objectToShare The object to set up as a network object. It will have an event named - * `onDidDispose` added to its properties. An error will be thrown if the object already had an - * `onDidDispose` property on it. If the object already contained a `dispose` function, a new - * `dispose` function will be set that calls the existing function (amongst other things). If the - * object did not already define a `dispose` function, one will be added. - * - * WARNING: setting a network object mutates the provided object. - * @returns `objectToShare` modified to be a network object - */ - const set: ( - id: string, - objectToShare: T, - objectType?: string, - objectAttributes?: - | { - [property: string]: unknown; - } - | undefined, - ) => Promise>; - interface NetworkObjectService { - initialize: typeof initialize; - hasKnown: typeof hasKnown; - get: typeof get; - set: typeof set; - onDidCreateNetworkObject: typeof onDidCreateNetworkObject; - } - /** - * Network objects are distributed objects within PAPI for TS/JS objects. @see - * https://en.wikipedia.org/wiki/Distributed_object - * - * Objects registered via {@link networkObjectService.set} are retrievable using - * {@link networkObjectService.get}. - * - * Function calls made on network objects retrieved via {@link networkObjectService.get} are proxied - * and sent to the original objects registered via {@link networkObjectService.set}. All functions on - * the registered object are proxied except for constructors, `dispose`, and functions starting with - * `on` since those should be events (which are not intended to be proxied) based on our naming - * convention. If you don't want a function to be proxied, don't make it a property of the - * registered object. - * - * Functions on a network object will be called asynchronously by other processes regardless of - * whether the functions are synchronous or asynchronous, so it is best to make them all - * asynchronous. All shared functions' arguments and return values must be serializable to be called - * across processes. - * - * When a service registers an object via {@link networkObjectService.set}, it is the responsibility - * of that service, and only that service, to call `dispose` on that object when it is no longer - * intended to be shared with other services. - * - * When an object is disposed by calling `dispose`, all functions registered with the `onDidDispose` - * event handler will be called. After an object is disposed, calls to its functions will no longer - * be proxied to the original object. - */ - const networkObjectService: NetworkObjectService; - export default networkObjectService; -} -declare module 'shared/models/network-object.model' { - import { - Dispose, - OnDidDispose, - CannotHaveOnDidDispose, - CanHaveOnDidDispose, - } from 'platform-bible-utils'; - /** - * An object of this type is returned from {@link networkObjectService.get}. - * - * Override the NetworkableObject type's force-undefined onDidDispose to NetworkObject's - * onDidDispose type because it will have an onDidDispose added. - * - * If an object of type T had `dispose` on it, `networkObjectService.get` will remove the ability to - * 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 - */ - export type NetworkObject = Omit, 'dispose'> & - OnDidDispose; - /** - * An object of this type is returned from {@link networkObjectService.set}. - * - * @see networkObjectService - */ - export type DisposableNetworkObject = NetworkObject & Dispose; - /** - * An object of this type is passed into {@link networkObjectService.set}. - * - * @see networkObjectService - */ - export type NetworkableObject = T & CannotHaveOnDidDispose; - /** - * If a network object with the provided ID exists remotely but has not been set up to use inside - * this process, this function is run in {@link networkObjectService.get}, and the returned object is - * used as a base on which to set up a NetworkObject for use on this process. All properties that - * are exposed in the base object will be used as-is, and all other properties will be assumed to - * exist on the remote network object. - * - * @param id ID of the network object to get - * @param networkObjectContainer Holds a reference to the NetworkObject that will be setup within - * {@link networkObjectService.get}. It is passed in to allow the return value to call functions on - * the NetworkObject. NOTE: networkObjectContainer.contents does not point to a real NetworkObject - * while this function is running. The real reference is assigned later, but before the - * NetworkObject will be used. The return value should always reference the NetworkObject as - * `networkObjectContainer.contents` to avoid acting upon an undefined NetworkObject. - * @returns The local object to proxy into a network object. - * - * 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 - */ - export type LocalObjectToProxyCreator = ( - id: string, - networkObjectPromise: Promise>, - ) => Partial; - /** Data about an object shared on the network */ - export type NetworkObjectDetails = { - /** ID of the network object that processes use to reference it */ - id: string; - /** - * Name of the type of this network object. Note this isn't about TypeScript types, but instead - * focused on the platform data model. Names of types for the same logical thing (e.g., Project - * Data Providers => `pdp`) should be the same across all process on the network regardless of - * what programming language they use. For generic network objects, `networkObject` is - * appropriate. - */ - objectType: string; - /** Array of strings with the function names exposed on this network object */ - functionNames: string[]; - /** - * Optional object containing properties that describe this network object. The properties - * associated with this network object depend on the `objectType`. - */ - attributes?: Record; - }; -} -declare module 'shared/models/data-provider.model' { - import { UnsubscriberAsync, PlatformEventHandler } from 'platform-bible-utils'; - import { NetworkableObject } from 'shared/models/network-object.model'; - /** Various options to adjust how the data provider subscriber emits updates */ - export type DataProviderSubscriberOptions = { - /** - * Whether to immediately retrieve the data for this subscriber and run the callback as soon as - * possible. - * - * This allows a subscriber to simply subscribe and provide a callback instead of subscribing, - * running `get`, and managing the race condition of an event coming in to update the data and the - * initial `get` coming back in. - * - * @default true - */ - retrieveDataImmediately?: boolean; - /** - * Under which conditions to run the callback when we receive updates to the data. - * - * - `'deeply-equal'` - only run the update callback when the data at this selector has changed. - * - * For example, suppose your selector is targeting John 3:5, and the data provider updates its - * data for Luke 5:3. Your data at John 3:5 does not change, and your callback will not run. - * - `'*'` - run the update callback every time the data has been updated whether or not the data at - * this selector has changed. - * - * For example, suppose your selector is targeting John 3:5, and the data provider updates its - * data for Luke 5:3. Your data at John 3:5 does not change, but your callback will run again - * with the same data anyway. - * - * @default 'deeply-equal' - */ - whichUpdates?: 'deeply-equal' | '*'; - }; - /** - * Information that papi uses to interpret whether to send out updates on a data provider when the - * engine runs `set` or `notifyUpdate`. - * - * - `'*'` update subscriptions for all data types on this data provider - * - `string` name of data type - update subscriptions for this data type - * - `string[]` names of data types - update subscriptions for the data types in the array - * - `true` (or other truthy values other than strings and arrays) - * - * - In `set` - update subscriptions for this data type - * - In `notifyUpdate` - same as '*' - * - `false` (or falsy) do not update subscriptions - */ - export type DataProviderUpdateInstructions = - | '*' - | DataTypeNames - | DataTypeNames[] - | boolean; - /** - * Set a subset of data according to the selector. - * - * Note: if a data provider engine does not provide `set` (possibly indicating it is read-only), - * this will throw an exception. - * - * @param selector Tells the provider what subset of data is being set - * @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 - */ - export type DataProviderSetter< - TDataTypes extends DataProviderDataTypes, - DataType extends keyof TDataTypes, - > = ( - selector: TDataTypes[DataType]['selector'], - data: TDataTypes[DataType]['setData'], - ) => Promise>; - /** - * Get a subset of data from the provider according to the selector. - * - * Note: This is good for retrieving data from a provider once. If you want to keep the data - * up-to-date, use `subscribe` instead, which can immediately give you the data and keep it - * up-to-date. - * - * @param selector Tells the provider what subset of data to get - * @returns The subset of data represented by the selector - */ - export type DataProviderGetter = ( - selector: TDataType['selector'], - ) => Promise; - /** - * Subscribe to receive updates relevant to the provided selector from this data provider for a - * specific data type. - * - * Note: By default, this `subscribe` function automatically retrieves the current state - * of the data 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 `get`. You can turn this - * functionality off in the `options` parameter. - * - * @param selector Tells the provider what data this listener is listening for - * @param callback Function to run with the updated data for this selector - * @param options Various options to adjust how the subscriber emits updates - * @returns Unsubscriber to stop listening for updates - */ - export type DataProviderSubscriber = ( - selector: TDataType['selector'], - callback: PlatformEventHandler, - options?: DataProviderSubscriberOptions, - ) => Promise; - /** - * A helper type describing the types associated with a data provider's methods for a specific data - * type it handles. - * - * @type `TSelector` - The type of selector used to get some data from this provider at this data - * type. A selector is an object a caller provides to the data provider to tell the provider what - * subset of data it wants at this data type. - * @type `TGetData` - The type of data provided by this data provider when you run `get` - * based on a provided selector - * @type `TSetData` - The type of data ingested by this data provider when you run `set` - * based on a provided selector - */ - export type DataProviderDataType< - TSelector = unknown, - TGetData = TSelector, - TSetData = TGetData, - > = { - /** - * The type of selector used to get some data from this provider at this data type. A selector is - * an object a caller provides to the data provider to tell the provider what subset of data it - * wants at this data type. - */ - selector: TSelector; - /** - * The type of data provided by this data provider when you run `get` based on a - * provided selector - */ - getData: TGetData; - /** - * The type of data ingested by this data provider when you run `set` based on a - * provided selector - */ - setData: TSetData; - }; - /** - * A helper type describing all the data types a data provider handles. Each property on this type - * (consisting of a DataProviderDataType, which describes the types that correspond to that data - * type) describes a data type that the data provider handles. The data provider has a - * `set`, `get`, and `subscribe` for each property (aka data type) - * listed in this type. - * - * @example A data provider that handles greeting strings and age numbers (as well as an All data - * type that just provides all the data) could have a DataProviderDataTypes that looks like the - * following: - * - * ```typescript - * { - * Greeting: DataProviderDataType; - * Age: DataProviderDataType; - * All: DataProviderDataType; - * } - * ``` - */ - export type DataProviderDataTypes = { - [dataType: string]: DataProviderDataType; - }; - /** - * 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 - */ - 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 - */ - export type DataProviderSetters = { - [DataType in keyof TDataTypes as `set${DataType & string}`]: DataProviderSetter< - TDataTypes, - DataType - >; - }; - /** - * Set of all `get` methods that a data provider provides according to its data types. - * - * @see DataProviderGetter for more information - */ - export type DataProviderGetters = { - [DataType in keyof TDataTypes as `get${DataType & string}`]: DataProviderGetter< - TDataTypes[DataType] - >; - }; - /** - * Set of all `subscribe` methods that a data provider provides according to its data - * types. - * - * @see DataProviderSubscriber for more information - */ - export type DataProviderSubscribers = { - [DataType in keyof TDataTypes as `subscribe${DataType & string}`]: DataProviderSubscriber< - TDataTypes[DataType] - >; - }; - /** - * An internal object created locally when someone runs dataProviderService.registerEngine. This - * 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 - */ - type DataProviderInternal = - NetworkableObject< - DataProviderSetters & - DataProviderGetters & - DataProviderSubscribers - >; - /** - * Get the data type for a data provider function based on its name - * - * @param fnName Name of data provider function e.g. `getVerse` - * @returns Data type for that data provider function e.g. `Verse` - */ - export function getDataProviderDataTypeFromFunctionName< - TDataTypes extends DataProviderDataTypes = DataProviderDataTypes, - >(fnName: string): DataTypeNames; - export default DataProviderInternal; -} -declare module 'shared/models/project-data-provider.model' { - import type { DataProviderDataType } 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 - * 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 PDP for accessing extension data. There - * is no way to get or set just a portion of data identified by a single dataQualifier value. - */ - dataQualifier: string; - }; - /** - * 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 - * - 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 - * - In the future, we may enforce that callers to `ExtensionData` endpoints include `extensionName`, - * so following this interface ensures your PDP will not break if such a requirement is - * implemented. - */ - export type MandatoryProjectDataType = { - ExtensionData: DataProviderDataType; - }; + export type DataProviderSubscribers = { + [DataType in keyof TDataTypes as `subscribe${DataType & string}`]: DataProviderSubscriber; + }; + /** + * An internal object created locally when someone runs dataProviderService.registerEngine. This + * 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 + */ + type DataProviderInternal = NetworkableObject & DataProviderGetters & DataProviderSubscribers>; + /** + * Get the data type for a data provider function based on its name + * + * @param fnName Name of data provider function e.g. `getVerse` + * @returns Data type for that data provider function e.g. `Verse` + */ + export function getDataProviderDataTypeFromFunctionName(fnName: string): DataTypeNames; + export default DataProviderInternal; +} +declare module "shared/models/project-data-provider.model" { + import type { DataProviderDataType } 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 + * 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 PDP for accessing extension data. There + * is no way to get or set just a portion of data identified by a single dataQualifier value. + */ + dataQualifier: string; + }; + /** + * 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 + * - 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 + * - In the future, we may enforce that callers to `ExtensionData` endpoints include `extensionName`, + * so following this interface ensures your PDP will not break if such a requirement is + * implemented. + */ + export type MandatoryProjectDataType = { + ExtensionData: DataProviderDataType; + }; } -declare module 'shared/models/data-provider.interface' { - import { - DataProviderDataTypes, - DataProviderGetters, - DataProviderSetters, - DataProviderSubscribers, - } from 'shared/models/data-provider.model'; - 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. - * - * Note: each `set` method has a corresponding `get` and - * `subscribe` method. - */ - type IDataProvider = - DataProviderSetters & - DataProviderGetters & - DataProviderSubscribers & - OnDidDispose; - export default IDataProvider; - /** - * A data provider that has control over disposing of it with dispose. Returned from registering a - * data provider (only the service that set it up should dispose of it) with - * dataProviderService.registerEngine - * - * @see IDataProvider - */ - export type IDisposableDataProvider> = TDataProvider & - Dispose; +declare module "shared/models/data-provider.interface" { + import { DataProviderDataTypes, DataProviderGetters, DataProviderSetters, DataProviderSubscribers } from "shared/models/data-provider.model"; + 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. + * + * Note: each `set` method has a corresponding `get` and + * `subscribe` method. + */ + type IDataProvider = DataProviderSetters & DataProviderGetters & DataProviderSubscribers & OnDidDispose; + export default IDataProvider; + /** + * A data provider that has control over disposing of it with dispose. Returned from registering a + * data provider (only the service that set it up should dispose of it) with + * dataProviderService.registerEngine + * + * @see IDataProvider + */ + export type IDisposableDataProvider> = TDataProvider & Dispose; } -declare module 'shared/models/data-provider-engine.model' { - import { - DataProviderDataTypes, - DataProviderGetters, - DataProviderUpdateInstructions, - DataProviderSetters, - } from 'shared/models/data-provider.model'; - import { NetworkableObject } from 'shared/models/network-object.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. - * - * @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: - * - * ```typescript - * notifyUpdate(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 - * - * WARNING: Do not update a data type in its `get` method (unless you make a base case)! - * It will create a destructive infinite loop. - */ - export type DataProviderEngineNotifyUpdate = ( - updateInstructions?: DataProviderUpdateInstructions, - ) => void; - /** - * Addon type for IDataProviderEngine to specify that there is a `notifyUpdate` method on the data - * 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. - */ - export type WithNotifyUpdate = { +declare module "shared/models/data-provider-engine.model" { + import { DataProviderDataTypes, DataProviderGetters, DataProviderUpdateInstructions, DataProviderSetters } from "shared/models/data-provider.model"; + import { NetworkableObject } from "shared/models/network-object.model"; /** + * 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 @@ -2018,2854 +1633,2332 @@ declare module 'shared/models/data-provider-engine.model' { * WARNING: Do not update a data type in its `get` method (unless you make a base case)! * It will create a destructive infinite loop. */ - notifyUpdate: DataProviderEngineNotifyUpdate; - }; - /** - * 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. - * - * @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 - * Intellisense. - * - * Note: papi creates a `notifyUpdate` function on the 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 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 - * your data provider engine like so: - * ```typescript - * const myDPE: IDataProviderEngine & WithNotifyUpdate = { - * notifyUpdate(updateInstructions) {}, - * ... - * } - * ``` - * OR - * ```typescript - * class MyDPE implements IDataProviderEngine { - * notifyUpdate(updateInstructions?: DataProviderEngineNotifyUpdate) {} - * ... - * } - * ``` - * - * 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 & - /** - * Set of all `set` methods that a data provider engine must provide according to its - * data types. papi overwrites this function on the DataProviderEngine itself to emit an update - * after running the defined `set` method in the DataProviderEngine. - * - * Note: papi requires that each `set` method has a corresponding `get` - * method. - * - * Note: to make a data type read-only, you can always return false or throw from - * `set`. - * - * 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 - */ - DataProviderSetters & - /** - * Set of all `get` methods that a data provider engine must provide according to its - * data types. Run by the data provider on `get` - * - * Note: papi requires that each `set` method has a corresponding `get` - * method. - * - * @see DataProviderGetter for more information - */ - DataProviderGetters & - Partial>; - export default IDataProviderEngine; -} -declare module 'shared/models/extract-data-provider-data-types.model' { - import IDataProviderEngine from 'shared/models/data-provider-engine.model'; - import IDataProvider, { IDisposableDataProvider } from 'shared/models/data-provider.interface'; - import DataProviderInternal from 'shared/models/data-provider.model'; - /** - * Get the `DataProviderDataTypes` associated with the `IDataProvider` - essentially, returns - * `TDataTypes` from `IDataProvider`. - * - * Works with generic types `IDataProvider`, `DataProviderInternal`, `IDisposableDataProvider`, and - * `IDataProviderEngine` along with the `papi-shared-types` extensible interfaces `DataProviders` - * and `DisposableDataProviders` - */ - type ExtractDataProviderDataTypes = - TDataProvider extends IDataProvider - ? TDataProviderDataTypes - : TDataProvider extends DataProviderInternal - ? TDataProviderDataTypes - : TDataProvider extends IDisposableDataProvider - ? TDataProviderDataTypes - : TDataProvider extends IDataProviderEngine - ? TDataProviderDataTypes - : never; - 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 { 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'; - /** - * 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 - * 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 - * following to its `.d.ts` file: - * - * @example - * - * ```typescript - * declare module 'papi-shared-types' { - * export interface CommandHandlers { - * 'myExtension.myCommand1': (foo: string, bar: number) => string; - * 'myExtension.myCommand2': (foo: string) => Promise; - * } - * } - * ``` - */ - interface CommandHandlers { - 'test.echo': (message: string) => string; - 'test.echoRenderer': (message: string) => Promise; - 'test.echoExtensionHost': (message: string) => Promise; - 'test.throwError': (message: string) => void; - 'platform.restartExtensionHost': () => Promise; - 'platform.quit': () => Promise; - 'test.addMany': (...nums: number[]) => number; - 'test.throwErrorExtensionHost': (message: string) => void; - } - /** - * Names for each command available on the papi. - * - * Automatically includes all extensions' commands that are added to {@link CommandHandlers}. - * - * @example 'platform.quit'; - */ - type CommandNames = keyof CommandHandlers; - interface SettingTypes { - 'platform.verseRef': ScriptureReference; - placeholder: undefined; - } - type SettingNames = keyof SettingTypes; - /** This is just a simple example so we have more than one. It's not intended to be real. */ - type NotesOnlyProjectDataTypes = MandatoryProjectDataType & { - 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. - * - * 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. - * - * 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): - * - * @example - * - * ```typescript - * declare module 'papi-shared-types' { - * export type MyProjectDataType = MandatoryProjectDataType & { - * MyProjectData: DataProviderDataType; - * }; - * - * export interface ProjectDataProviders { - * MyExtensionProjectTypeName: IDataProvider; - * } - * } - * ``` - */ - interface ProjectDataProviders { - 'platform.notesOnly': IDataProvider; - 'platform.placeholder': IDataProvider; - } - /** - * Names for each project data provider available on the papi. - * - * Automatically includes all extensions' project data providers that are added to - * {@link ProjectDataProviders}. - * - * @example 'platform.placeholder' - */ - type ProjectTypes = keyof ProjectDataProviders; - /** - * `DataProviderDataTypes` for each project data provider supported by PAPI. These are the data - * types served by each project data provider. - * - * Automatically includes all extensions' project data providers that are added to - * {@link ProjectDataProviders}. - * - * @example - * - * ```typescript - * ProjectDataTypes['MyExtensionProjectTypeName'] => { - * MyProjectData: DataProviderDataType; - * } - * ``` - */ - type ProjectDataTypes = { - [ProjectType in ProjectTypes]: ExtractDataProviderDataTypes; - }; - type StuffDataTypes = { - Stuff: DataProviderDataType; - }; - type PlaceholderDataTypes = { - Placeholder: DataProviderDataType< - { - thing: number; - }, - string[], - number - >; - }; - /** - * `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 - * 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 data provider it registers by - * adding the following to its `.d.ts` file (in this example, we are adding the - * `'helloSomeone.people'` data provider types): - * - * @example - * - * ```typescript - * declare module 'papi-shared-types' { - * export type PeopleDataTypes = { - * Greeting: DataProviderDataType; - * Age: DataProviderDataType; - * People: DataProviderDataType; - * }; - * - * export type PeopleDataMethods = { - * deletePerson(name: string): Promise; - * testRandomMethod(things: string): Promise; - * }; - * - * export type PeopleDataProvider = IDataProvider & PeopleDataMethods; - * - * export interface DataProviders { - * 'helloSomeone.people': PeopleDataProvider; - * } - * } - * ``` - */ - interface DataProviders { - 'platform.stuff': IDataProvider; - 'platform.placeholder': IDataProvider; - } - /** - * Names for each data provider available on the papi. - * - * Automatically includes all extensions' data providers that are added to {@link DataProviders}. - * - * @example 'platform.placeholder' - */ - type DataProviderNames = keyof DataProviders; - /** - * `DataProviderDataTypes` for each data provider supported by PAPI. These are the data types - * served by each data provider. - * - * Automatically includes all extensions' data providers that are added to {@link DataProviders}. - * - * @example - * - * ```typescript - * DataProviderTypes['helloSomeone.people'] => { - * Greeting: DataProviderDataType; - * Age: DataProviderDataType; - * People: DataProviderDataType; - * } - * ``` - */ - type DataProviderTypes = { - [DataProviderName in DataProviderNames]: ExtractDataProviderDataTypes< - DataProviders[DataProviderName] - >; - }; - /** - * Disposable version of each data provider type supported by PAPI. These objects are only - * returned from `papi.dataProviders.registerEngine` - only the one who registers a data provider - * engine is allowed to dispose of the data provider. - * - * Automatically includes all extensions' data providers that are added to {@link DataProviders}. - */ - type DisposableDataProviders = { - [DataProviderName in DataProviderNames]: IDisposableDataProvider< - DataProviders[DataProviderName] - >; - }; -} -declare module 'shared/services/command.service' { - import { UnsubscriberAsync } from 'platform-bible-utils'; - import { CommandHandlers, CommandNames } from 'papi-shared-types'; - module 'papi-shared-types' { - interface CommandHandlers { - 'test.addThree': typeof addThree; - 'test.squareAndConcat': typeof squareAndConcat; - } - } - function addThree(a: number, b: number, c: number): Promise; - function squareAndConcat(a: number, b: string): Promise; - /** Sets up the CommandService. Only runs once and always returns the same promise after that */ - export const initialize: () => Promise; - /** Send a command to the backend. */ - export const sendCommand: ( - commandName: CommandName, - ...args: Parameters - ) => Promise>>; - /** - * Creates a function that is a command function with a baked commandName. This is also nice because - * you get TypeScript type support using this function. - * - * @param commandName Command name for command function - * @returns Function to call with arguments of command that sends the command and resolves with the - * result of the command - */ - export const createSendCommandFunction: ( - commandName: CommandName, - ) => ( - ...args: Parameters - ) => Promise>>; - /** - * Register a command on the papi to be handled here - * - * @param commandName Command name to register for handling here - * - * - Note: Command 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. - * - * @param handler Function to run when the command is invoked - * @returns True if successfully registered, throws with error message if not - */ - export const registerCommand: ( - commandName: CommandName, - handler: CommandHandlers[CommandName], - ) => Promise; - /** - * - * The command service allows you to exchange messages with other components in the platform. You - * can register a command that other services and extensions can send you. You can send commands to - * other services and extensions that have registered commands. - */ - export type moduleSummaryComments = {}; -} -declare module 'shared/models/docking-framework.model' { - import { MutableRefObject, ReactNode } from 'react'; - import { DockLayout, DropDirection, LayoutBase } from 'rc-dock'; - import { - SavedWebViewDefinition, - WebViewDefinition, - WebViewDefinitionUpdateInfo, - } from 'shared/models/web-view.model'; - /** - * Saved information used to recreate a tab. - * - * - {@link TabLoader} loads this into {@link TabInfo} - * - {@link TabSaver} saves {@link TabInfo} into this - */ - export type SavedTabInfo = { - /** - * Tab ID - a unique identifier that identifies this tab. If this tab is a WebView, this ID will - * match the `WebViewDefinition.id` - */ - id: string; - /** Type of tab - indicates what kind of built-in tab this info represents */ - tabType: string; - /** Data needed to load the tab */ - data?: unknown; - }; - /** - * Information that Paranext uses to create a tab in the dock layout. - * - * - {@link TabLoader} loads {@link SavedTabInfo} into this - * - {@link TabSaver} saves this into {@link SavedTabInfo} - */ - export type TabInfo = SavedTabInfo & { - /** - * Url of image to show on the title bar of the tab - * - * Defaults to Platform.Bible logo - */ - tabIconUrl?: string; - /** Text to show on the title bar of the tab */ - tabTitle: string; - /** Text to show when hovering over the title bar of the tab */ - tabTooltip?: string; - /** Content to show inside the tab. */ - content: ReactNode; - /** (optional) Minimum width that the tab can become in CSS `px` units */ - minWidth?: number; - /** (optional) Minimum height that the tab can become in CSS `px` units */ - minHeight?: number; - }; - /** - * Function that takes a {@link SavedTabInfo} and creates a Paranext tab out of it. Each type of tab - * must provide a {@link TabLoader}. - * - * For now all tab creators must do their own data type verification - */ - export type TabLoader = (savedTabInfo: SavedTabInfo) => TabInfo; - /** - * Function that takes a Paranext tab and creates a saved tab out of it. Each type of tab can - * provide a {@link TabSaver}. If they do not provide one, the properties added by `TabInfo` are - * stripped from TabInfo by `saveTabInfoBase` before saving (so it is just a {@link SavedTabInfo}). - * - * @param tabInfo The Paranext tab to save - * @returns The saved tab info for Paranext to persist. If `undefined`, does not save the tab - */ - export type TabSaver = (tabInfo: TabInfo) => SavedTabInfo | undefined; - /** Information about a tab in a panel */ - interface TabLayout { - type: 'tab'; - } - /** - * Indicates where to display a floating window - * - * - `cascade` - place the window a bit below and to the right of the previously created floating - * window - * - `center` - center the window in the dock layout - */ - type FloatPosition = 'cascade' | 'center'; - /** The dimensions for a floating tab in CSS `px` units */ - export type FloatSize = { - width: number; - height: number; - }; - /** Information about a floating window */ - export interface FloatLayout { - type: 'float'; - floatSize?: FloatSize; - /** Where to display the floating window. Defaults to `cascade` */ - position?: FloatPosition; - } - export type PanelDirection = - | 'left' - | 'right' - | 'bottom' - | 'top' - | 'before-tab' - | 'after-tab' - | 'maximize' - | 'move' - | 'active' - | 'update'; - /** Information about a panel */ - interface PanelLayout { - type: 'panel'; - direction?: PanelDirection; - /** If undefined, it will add in the `direction` relative to the previously added tab. */ - targetTabId?: string; - } - /** Information about how a Paranext tab fits into the dock layout */ - export type Layout = TabLayout | FloatLayout | PanelLayout; - /** Event emitted when webViews are created */ - export type AddWebViewEvent = { - webView: SavedWebViewDefinition; - layout: Layout; - }; - /** Props that are passed to the web view tab component */ - export type WebViewTabProps = WebViewDefinition; - /** Rc-dock's onLayoutChange prop made asynchronous - resolves */ - export type OnLayoutChangeRCDock = ( - newLayout: LayoutBase, - currentTabId?: string, - direction?: DropDirection, - ) => Promise; - /** Properties related to the dock layout */ - export type PapiDockLayout = { - /** The rc-dock dock layout React element ref. Used to perform operations on the layout */ - dockLayout: DockLayout; - /** - * A ref to a function that runs when the layout changes. We set this ref to our - * {@link onLayoutChange} function - */ - onLayoutChangeRef: MutableRefObject; - /** - * Add or update a tab in the layout - * - * @param savedTabInfo Info for tab to add or update - * @param layout Information about where to put a new tab - * @returns If tab added, final layout used to display the new tab. If existing tab updated, - * `undefined` - */ - addTabToDock: (savedTabInfo: SavedTabInfo, layout: Layout) => Layout | undefined; - /** - * Add or update a webview in the layout - * - * @param webView Web view to add or update - * @param layout Information about where to put a new webview - * @returns If WebView added, final layout used to display the new webView. If existing webView - * updated, `undefined` - */ - addWebViewToDock: (webView: WebViewTabProps, layout: Layout) => Layout | undefined; - /** - * Remove a tab in the layout - * - * @param tabId ID of the tab to remove - */ - removeTabFromDock: (tabId: string) => boolean; - /** - * Gets the WebView definition for the web view with the specified ID - * - * @param webViewId The ID of the WebView whose web view definition to get - * @returns WebView definition with the specified ID or undefined if not found - */ - getWebViewDefinition: (webViewId: string) => WebViewDefinition | undefined; - /** - * Updates the WebView with the specified ID with the specified properties - * - * @param webViewId The ID of the WebView to update - * @param updateInfo Properties to update on the WebView. Any unspecified properties will stay the - * same - * @returns True if successfully found the WebView to update; false otherwise - */ - updateWebViewDefinition: ( - webViewId: string, - updateInfo: WebViewDefinitionUpdateInfo, - ) => boolean; - /** - * The layout to use as the default layout if the dockLayout doesn't have a layout loaded. - * - * TODO: This should be removed and the `testLayout` imported directly in this file once this - * service is refactored to split the code between processes. The only reason this is passed from - * `platform-dock-layout.component.tsx` is that we cannot import `testLayout` here since this - * service is currently all shared code. Refactor should happen in #203 - */ - testLayout: LayoutBase; - }; -} -declare module 'shared/services/web-view.service-model' { - import { GetWebViewOptions, WebViewId, WebViewType } from 'shared/models/web-view.model'; - import { AddWebViewEvent, Layout } from 'shared/models/docking-framework.model'; - import { PlatformEvent } from 'platform-bible-utils'; - /** - * - * Service exposing various functions related to using webViews - * - * WebViews are iframes in the Platform.Bible UI into which extensions load frontend code, either - * HTML or React components. - */ - export interface WebViewServiceType { - /** Event that emits with webView info when a webView is added */ - onDidAddWebView: PlatformEvent; - /** - * Creates a new web view or gets an existing one depending on if you request an existing one and - * if the web view provider decides to give that existing one to you (it is up to the provider). - * - * @param webViewType Type of WebView to create - * @param layout Information about where you want the web view to go. Defaults to adding as a tab - * @param options Options that affect what this function does. For example, you can provide an - * existing web view ID to request an existing web view with that ID. - * @returns Promise that resolves to the ID of the webview we got or undefined if the provider did - * not create a WebView for this request. - * @throws If something went wrong like the provider for the webViewType was not found - */ - getWebView: ( - webViewType: WebViewType, - layout?: Layout, - options?: GetWebViewOptions, - ) => Promise; - } - /** Name to use when creating a network event that is fired when webViews are created */ - export const EVENT_NAME_ON_DID_ADD_WEB_VIEW: `${string}:${string}`; - export const NETWORK_OBJECT_NAME_WEB_VIEW_SERVICE = 'WebViewService'; -} -declare module 'shared/services/network-object-status.service-model' { - import { NetworkObjectDetails } from 'shared/models/network-object.model'; - export interface NetworkObjectStatusRemoteServiceType { + export type DataProviderEngineNotifyUpdate = (updateInstructions?: DataProviderUpdateInstructions) => void; /** - * Get details about all available network objects + * Addon type for IDataProviderEngine to specify that there is a `notifyUpdate` method on the data + * 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`. * - * @returns Object whose keys are the names of the network objects and whose values are the - * {@link NetworkObjectDetails} for each network object + * @see DataProviderEngineNotifyUpdate for more information on `notifyUpdate`. + * @see IDataProviderEngine for more information on using this type. */ - getAllNetworkObjectDetails: () => Promise>; - } - /** Provides functions related to the set of available network objects */ - export interface NetworkObjectStatusServiceType extends NetworkObjectStatusRemoteServiceType { + export type WithNotifyUpdate = { + /** JSDOC DESTINATION DataProviderEngineNotifyUpdate */ + notifyUpdate: DataProviderEngineNotifyUpdate; + }; /** - * Get a promise that resolves when a network object is registered or rejects if a timeout is hit + * 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. + * + * @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 + * Intellisense. + * + * Note: papi creates a `notifyUpdate` function on the 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 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 + * your data provider engine like so: + * ```typescript + * const myDPE: IDataProviderEngine & WithNotifyUpdate = { + * notifyUpdate(updateInstructions) {}, + * ... + * } + * ``` + * OR + * ```typescript + * class MyDPE implements IDataProviderEngine { + * notifyUpdate(updateInstructions?: DataProviderEngineNotifyUpdate) {} + * ... + * } + * ``` * - * @returns Promise that either resolves to the {@link NetworkObjectDetails} for a network object - * once the network object is registered, or rejects if a timeout is provided and the timeout is - * reached before the network object is registered + * 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 { + * ... + * } + * ``` */ - waitForNetworkObject: (id: string, timeoutInMS?: number) => Promise; - } - export const networkObjectStatusServiceNetworkObjectName = 'NetworkObjectStatusService'; -} -declare module 'shared/services/network-object-status.service' { - import { NetworkObjectStatusServiceType } from 'shared/services/network-object-status.service-model'; - const networkObjectStatusService: NetworkObjectStatusServiceType; - export default networkObjectStatusService; -} -declare module 'shared/services/web-view.service' { - import { WebViewServiceType } from 'shared/services/web-view.service-model'; - const webViewService: WebViewServiceType; - export default webViewService; -} -declare module 'shared/models/web-view-provider.model' { - import { - GetWebViewOptions, - WebViewDefinition, - SavedWebViewDefinition, - } from 'shared/models/web-view.model'; - import { - DisposableNetworkObject, - NetworkObject, - NetworkableObject, - } from 'shared/models/network-object.model'; - import { CanHaveOnDidDispose } from 'platform-bible-utils'; - export interface IWebViewProvider extends NetworkableObject { - /** - * @param savedWebView Filled out if an existing webview is being called for (matched by ID). Just - * ID if this is a new request or if the web view with the existing ID was not found - * @param getWebViewOptions - */ - getWebView( - savedWebView: SavedWebViewDefinition, - getWebViewOptions: GetWebViewOptions, - ): Promise; - } - export interface WebViewProvider - extends NetworkObject, - CanHaveOnDidDispose {} - export interface DisposableWebViewProvider - extends DisposableNetworkObject, - Omit {} -} -declare module 'shared/services/web-view-provider.service' { - /** - * Handles registering web view providers and serving web views around the papi. Exposed on the - * papi. - */ - import { - DisposableWebViewProvider, - IWebViewProvider, - WebViewProvider, - } from 'shared/models/web-view-provider.model'; - /** Sets up the service. Only runs once and always returns the same promise after that */ - const initialize: () => Promise; - /** - * Indicate if we are aware of an existing web view provider with the given type. If a web view - * provider with the given type 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. - * - * @param webViewType Type of webView to check for - */ - function hasKnown(webViewType: string): boolean; - /** - * Register a web view provider to serve webViews for a specified type of webViews - * - * @param webViewType Type of web view to provide - * @param webViewProvider Object to register as a webView provider including control over disposing - * of it. - * - * WARNING: setting a webView provider mutates the provided object. - * @returns `webViewProvider` modified to be a network object - */ - function register( - webViewType: string, - webViewProvider: IWebViewProvider, - ): Promise; - /** - * Get a web view provider that has previously been set up - * - * @param webViewType Type of webview provider to get - * @returns Web view provider with the given name if one exists, undefined otherwise - */ - function get(webViewType: string): Promise; - export interface WebViewProviderService { - initialize: typeof initialize; - hasKnown: typeof hasKnown; - register: typeof register; - get: typeof get; - } - export interface PapiWebViewProviderService { - register: typeof register; - } - const webViewProviderService: WebViewProviderService; - /** - * - * Interface for registering webView providers - */ - export const papiWebViewProviderService: PapiWebViewProviderService; - export default webViewProviderService; -} -declare module 'shared/services/internet.service' { - /** Our shim over fetch. Allows us to control internet access. */ - const papiFetch: typeof fetch; - export interface InternetService { - fetch: typeof papiFetch; - } - /** - * - * Service that provides a way to call `fetch` since the original function is not available - */ - const internetService: InternetService; - export default internetService; -} -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, - } from 'shared/models/data-provider-engine.model'; - import { - DataProviderNames, - DataProviderTypes, - DataProviders, - DisposableDataProviders, - } from 'papi-shared-types'; - 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 - * - * @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 - */ - function ignore( - method: Function & { - isIgnored?: boolean; - }, - ): void; - /** - * 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 - * - * @param target The class that has the method to ignore - * @param member The name of the method to ignore - * - * 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; - /** - * 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. - */ - const decorators: { - ignore: typeof ignore; - }; - /** - * 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. - */ - function registerEngine( - providerName: DataProviderName, - dataProviderEngine: IDataProviderEngine, - dataProviderType?: string, - dataProviderAttributes?: - | { - [property: string]: unknown; - } - | undefined, - ): Promise; - /** - * Creates a data provider to be shared on the network layering over the provided data provider - * engine. - * - * @type `TDataTypes` - The data provider data types served by the data provider to create. - * - * This is not exposed on the papi as it is a helper function to enable other services to layer over - * this service and create their own subsets of data providers with other types than - * `DataProviders` types using this function and {@link getByType} - * @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. - */ - export function registerEngineByType( - providerName: string, - dataProviderEngine: IDataProviderEngine, - dataProviderType?: string, - dataProviderAttributes?: - | { - [property: string]: unknown; - } - | undefined, - ): Promise>>; - /** - * 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 - */ - function get( - providerName: DataProviderName, - ): Promise; - /** - * Get a data provider that has previously been set up - * - * @type `T` - The type of data provider to get. Use `IDataProvider`, - * specifying your own types, or provide a custom data provider type - * - * This is not exposed on the papi as it is a helper function to enable other services to layer over - * this service and create their own subsets of data providers with other types than - * `DataProviders` types using this function and {@link registerEngineByType} - * @param providerName Name of the desired data provider - * @returns The data provider with the given name if one exists, undefined otherwise - */ - export function getByType>( - providerName: string, - ): Promise; - export interface DataProviderService { - hasKnown: typeof hasKnown; - registerEngine: typeof registerEngine; - get: typeof get; - decorators: typeof decorators; - DataProviderEngine: typeof DataProviderEngine; - } - /** - * - * Service that allows extensions to send and receive data to/from other extensions - */ - const dataProviderService: DataProviderService; - 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 */ - export type ProjectDataProviderEngineTypes = { - [ProjectType in ProjectTypes]: IDataProviderEngine; - }; - export interface ProjectDataProviderEngineFactory { - createProjectDataProviderEngine( - projectId: string, - projectStorageInterpreterId: string, - ): ProjectDataProviderEngineTypes[ProjectType]; - } -} -declare module 'shared/models/project-metadata.model' { - import { ProjectTypes } from 'papi-shared-types'; - /** - * Low-level information describing a project that Platform.Bible directly manages and uses to load - * project data - */ - export type ProjectMetadata = { - /** ID of the project (must be unique and case insensitive) */ - id: string; - /** Short name of the project (not necessarily unique) */ - name: string; - /** Indicates how the project is persisted to storage */ - storageType: string; - /** - * Indicates what sort of project this is which implies its data shape (e.g., what data streams - * should be available) - */ - projectType: ProjectTypes; - }; -} -declare module 'shared/services/project-lookup.service-model' { - import { ProjectMetadata } from 'shared/models/project-metadata.model'; - /** - * - * Provides metadata for projects known by the platform - */ - export interface ProjectLookupServiceType { + type IDataProviderEngine = NetworkableObject & /** - * Provide metadata for all projects found on the local system + * Set of all `set` methods that a data provider engine must provide according to its + * data types. papi overwrites this function on the DataProviderEngine itself to emit an update + * after running the defined `set` method in the DataProviderEngine. + * + * Note: papi requires that each `set` method has a corresponding `get` + * method. * - * @returns ProjectMetadata for all projects stored on the local system + * Note: to make a data type read-only, you can always return false or throw from + * `set`. + * + * 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 */ - getMetadataForAllProjects: () => Promise; + DataProviderSetters & /** - * Look up metadata for a specific project ID + * Set of all `get` methods that a data provider engine must provide according to its + * data types. Run by the data provider on `get` * - * @param projectId ID of the project to load - * @returns ProjectMetadata from the 'meta.json' file for the given project + * Note: papi requires that each `set` method has a corresponding `get` + * method. + * + * @see DataProviderGetter for more information */ - getMetadataForProject: (projectId: string) => Promise; - } - export const projectLookupServiceNetworkObjectName = 'ProjectLookupService'; -} -declare module 'shared/services/project-lookup.service' { - import { ProjectLookupServiceType } from 'shared/services/project-lookup.service-model'; - const projectLookupService: ProjectLookupServiceType; - export default projectLookupService; + DataProviderGetters & Partial>; + export default IDataProviderEngine; } -declare module 'shared/services/project-data-provider.service' { - import { ProjectTypes, ProjectDataProviders } from 'papi-shared-types'; - import { ProjectDataProviderEngineFactory } from 'shared/models/project-data-provider-engine.model'; - import { Dispose } from 'platform-bible-utils'; - /** - * Add a new Project Data Provider Factory to PAPI that uses the given engine. There must not be an - * existing factory already that handles the same project type or this operation will fail. - * - * @param projectType Type of project that pdpEngineFactory supports - * @param pdpEngineFactory Used in a ProjectDataProviderFactory to create ProjectDataProviders - * @returns Promise that resolves to a disposable object when the registration operation completes - */ - export function registerProjectDataProviderEngineFactory( - projectType: ProjectType, - pdpEngineFactory: ProjectDataProviderEngineFactory, - ): Promise; - /** - * Get a Project Data Provider for the given project ID. - * - * @example - * - * ```typescript - * const pdp = await get('ParatextStandard', 'ProjectID12345'); - * pdp.getVerse(new VerseRef('JHN', '1', '1')); - * ``` - * - * @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 - * project's actual `projectType` (according to its metadata), a warning will be logged - * @param projectId ID for the project to load - * @returns Data provider with types that are associated with the given project type - */ - export function get( - projectType: ProjectType, - projectId: string, - ): Promise; - export interface PapiBackendProjectDataProviderService { - registerProjectDataProviderEngineFactory: typeof registerProjectDataProviderEngineFactory; - get: typeof get; - } - /** - * - * Service that registers and gets project data providers - */ - export const papiBackendProjectDataProviderService: PapiBackendProjectDataProviderService; - export interface PapiFrontendProjectDataProviderService { - get: typeof get; - } - /** - * - * Service that gets project data providers - */ - export const papiFrontendProjectDataProviderService: { - get: typeof get; - }; -} -declare module 'shared/data/file-system.model' { - /** Types to use with file system operations */ - /** - * Represents a path in file system or other. Has a scheme followed by :// followed by a relative - * path. If no scheme is provided, the app scheme is used. Available schemes are as follows: - * - * - `app://` - goes to the app's home directory and into `.platform.bible` (platform-dependent) - * - `cache://` - goes to the app's temporary file cache at `app://cache` - * - `data://` - goes to the app's data storage location at `app://data` - * - `resources://` - goes to the resources directory installed in the app - * - `file://` - an absolute file path from root - */ - export type Uri = string; -} -declare module 'node/utils/util' { - import { Uri } from 'shared/data/file-system.model'; - export const FILE_PROTOCOL = 'file://'; - export const RESOURCES_PROTOCOL = 'resources://'; - export function resolveHtmlPath(htmlFileName: string): string; - /** - * Gets the platform-specific user Platform.Bible folder for this application - * - * When running in development: `/dev-appdata` - * - * When packaged: `/.platform.bible` - */ - export const getAppDir: import('memoize-one').MemoizedFn<() => string>; - /** - * Resolves the uri to a path - * - * @param uri The uri to resolve - * @returns Real path to the uri supplied - */ - export function getPathFromUri(uri: Uri): string; - /** - * Combines the uri passed in with the paths passed in to make one uri - * - * @param uri Uri to start from - * @param paths Paths to combine into the uri - * @returns One uri that combines the uri and the paths in left-to-right order - */ - export function joinUriPaths(uri: Uri, ...paths: string[]): Uri; -} -declare module 'node/services/node-file-system.service' { - /** File system calls from Node */ - import fs, { BigIntStats } from 'fs'; - import { Uri } from 'shared/data/file-system.model'; - /** - * Read a text file - * - * @param uri URI of file - * @returns Promise that resolves to the contents of the file - */ - export function readFileText(uri: Uri): Promise; - /** - * Read a binary file - * - * @param uri URI of file - * @returns Promise that resolves to the contents of the file - */ - export function readFileBinary(uri: Uri): Promise; - /** - * Write data to a file - * - * @param uri URI of file - * @param fileContents String or Buffer to write into the file - * @returns Promise that resolves after writing the file - */ - export function writeFile(uri: Uri, fileContents: string | Buffer): Promise; - /** - * Copies a file from one location to another. Creates the path to the destination if it does not - * exist - * - * @param sourceUri The location of the file to copy - * @param destinationUri The uri to the file to create as a copy of the source file - * @param mode Bitwise modifiers that affect how the copy works. See - * [`fsPromises.copyFile`](https://nodejs.org/api/fs.html#fspromisescopyfilesrc-dest-mode) for - * more information - */ - export function copyFile( - sourceUri: Uri, - destinationUri: Uri, - mode?: Parameters[2], - ): Promise; - /** - * Delete a file if it exists - * - * @param uri URI of file - * @returns Promise that resolves when the file is deleted or determined to not exist - */ - export function deleteFile(uri: Uri): Promise; - /** - * Get stats about the file or directory. Note that BigInts are used instead of ints to avoid. - * https://en.wikipedia.org/wiki/Year_2038_problem - * - * @param uri URI of file or directory - * @returns Promise that resolves to object of type https://nodejs.org/api/fs.html#class-fsstats if - * file or directory exists, undefined if it doesn't - */ - export function getStats(uri: Uri): Promise; - /** - * Set the last modified and accessed times for the file or directory - * - * @param uri URI of file or directory - * @returns Promise that resolves once the touch operation finishes - */ - export function touch(uri: Uri, date: Date): Promise; - /** Type of file system item in a directory */ - export enum EntryType { - File = 'file', - Directory = 'directory', - Unknown = 'unknown', - } - /** All entries in a directory, mapped from entry type to array of uris for the entries */ - export type DirectoryEntries = Readonly<{ - [entryType in EntryType]: Uri[]; - }>; - /** - * Reads a directory and returns lists of entries in the directory by entry type. - * - * @param uri - URI of directory. - * @param entryFilter - Function to filter out entries in the directory based on their names. - * @returns Map of entry type to list of uris for each entry in the directory with that type. - */ - export function readDir( - uri: Uri, - entryFilter?: (entryName: string) => boolean, - ): Promise; - /** - * Create a directory in the file system if it does not exist. Does not throw if it already exists. - * - * @param uri URI of directory - * @returns Promise that resolves once the directory has been created - */ - export function createDir(uri: Uri): Promise; - /** - * Remove a directory and all its contents recursively from the file system - * - * @param uri URI of directory - * @returns Promise that resolves when the delete operation finishes - */ - export function deleteDir(uri: Uri): Promise; -} -declare module 'node/utils/crypto-util' { - export function createUuid(): string; - /** - * Create a cryptographically secure nonce that is at least 128 bits long. See nonce spec at - * https://w3c.github.io/webappsec-csp/#security-nonces - * - * @param encoding: "base64url" (HTML safe, shorter string) or "hex" (longer string) From - * https://base64.guru/standards/base64url, the purpose of this encoding is "the ability to use - * the encoding result as filename or URL address" - * @param numberOfBytes: Number of bytes the resulting nonce should contain - * @returns Cryptographically secure, pseudo-randomly generated value encoded as a string - */ - export function createNonce(encoding: 'base64url' | 'hex', numberOfBytes?: number): string; -} -declare module 'node/models/execution-token.model' { - /** For now this is just for extensions, but maybe we will want to expand this in the future */ - export type ExecutionTokenType = 'extension'; - /** Execution tokens can be passed into API calls to provide context about their identity */ - export class ExecutionToken { - readonly type: ExecutionTokenType; - readonly name: string; - readonly nonce: string; - constructor(tokenType: ExecutionTokenType, name: string); - getHash(): string; - } +declare module "shared/models/extract-data-provider-data-types.model" { + import IDataProviderEngine from "shared/models/data-provider-engine.model"; + import IDataProvider, { IDisposableDataProvider } from "shared/models/data-provider.interface"; + import DataProviderInternal from "shared/models/data-provider.model"; + /** + * Get the `DataProviderDataTypes` associated with the `IDataProvider` - essentially, returns + * `TDataTypes` from `IDataProvider`. + * + * Works with generic types `IDataProvider`, `DataProviderInternal`, `IDisposableDataProvider`, and + * `IDataProviderEngine` along with the `papi-shared-types` extensible interfaces `DataProviders` + * and `DisposableDataProviders` + */ + type ExtractDataProviderDataTypes = TDataProvider extends IDataProvider ? TDataProviderDataTypes : TDataProvider extends DataProviderInternal ? TDataProviderDataTypes : TDataProvider extends IDisposableDataProvider ? TDataProviderDataTypes : TDataProvider extends IDataProviderEngine ? TDataProviderDataTypes : never; + export default ExtractDataProviderDataTypes; } -declare module 'node/services/execution-token.service' { - import { ExecutionToken } from 'node/models/execution-token.model'; - /** - * This should be called when extensions are being loaded - * - * @param extensionName Name of the extension to register - * @returns Token that can be passed to `tokenIsValid` to authenticate or authorize API callers. It - * is important that the token is not shared to avoid impersonation of API callers. - */ - function registerExtension(extensionName: string): ExecutionToken; - /** - * Remove a registered token. Note that a hash of a token is what is needed to unregister, not the - * full token itself (notably not the nonce), so something can be delegated the ability to - * unregister a token without having been given the full token itself. - * - * @param extensionName Name of the extension that was originally registered - * @param tokenHash Value of `getHash()` of the token that was originally registered. - * @returns `true` if the token was successfully unregistered, `false` otherwise - */ - function unregisterExtension(extensionName: string, tokenHash: string): boolean; - /** - * This should only be needed by services that need to contextualize the response for the caller - * - * @param executionToken Token that was previously registered. - * @returns `true` if the token matches a token that was previous registered, `false` otherwise. - */ - function tokenIsValid(executionToken: ExecutionToken): boolean; - const executionTokenService: { - registerExtension: typeof registerExtension; - unregisterExtension: typeof unregisterExtension; - tokenIsValid: typeof tokenIsValid; - }; - export default executionTokenService; -} -declare module 'extension-host/services/extension-storage.service' { - import { ExecutionToken } from 'node/models/execution-token.model'; - import { Buffer } from 'buffer'; - /** - * This is only intended to be called by the extension service. This service cannot call into the - * extension service or it causes a circular dependency. - */ - export function setExtensionUris(urisPerExtension: Map): void; - /** Return a path to the specified file within the extension's installation directory */ - export function buildExtensionPathFromName(extensionName: string, fileName: string): string; - /** - * Read a text file from the the extension's installation directory - * - * @param token ExecutionToken provided to the extension when `activate()` was called - * @param fileName Name of the file to be read - * @returns Promise for a string with the contents of the file - */ - function readTextFileFromInstallDirectory( - token: ExecutionToken, - fileName: string, - ): Promise; - /** - * Read a binary file from the the extension's installation directory - * - * @param token ExecutionToken provided to the extension when `activate()` was called - * @param fileName Name of the file to be read - * @returns Promise for a Buffer with the contents of the file - */ - function readBinaryFileFromInstallDirectory( - token: ExecutionToken, - fileName: string, - ): Promise; - /** - * Read data specific to the user (as identified by the OS) and extension (as identified by the - * ExecutionToken) - * - * @param token ExecutionToken provided to the extension when `activate()` was called - * @param key Unique identifier of the data - * @returns Promise for a string containing the data - */ - function readUserData(token: ExecutionToken, key: string): Promise; - /** - * Write data specific to the user (as identified by the OS) and extension (as identified by the - * ExecutionToken) - * - * @param token ExecutionToken provided to the extension when `activate()` was called - * @param key Unique identifier of the data - * @param data Data to be written - * @returns Promise that will resolve if the data is written successfully - */ - function writeUserData(token: ExecutionToken, key: string, data: string): Promise; - /** - * Delete data previously written that is specific to the user (as identified by the OS) and - * extension (as identified by the ExecutionToken) - * - * @param token ExecutionToken provided to the extension when `activate()` was called - * @param key Unique identifier of the data - * @returns Promise that will resolve if the data is deleted successfully - */ - function deleteUserData(token: ExecutionToken, key: string): Promise; - export interface ExtensionStorageService { - readTextFileFromInstallDirectory: typeof readTextFileFromInstallDirectory; - readBinaryFileFromInstallDirectory: typeof readBinaryFileFromInstallDirectory; - readUserData: typeof readUserData; - writeUserData: typeof writeUserData; - deleteUserData: typeof deleteUserData; - } - /** - * - * This service provides extensions in the extension host the ability to read/write data based on - * the extension identity and current user (as identified by the OS). This service will not work - * within the renderer. - */ - const extensionStorageService: ExtensionStorageService; - export default extensionStorageService; -} -declare module 'shared/models/dialog-options.model' { - /** General options to adjust dialogs (created from `papi.dialogs`) */ - export type DialogOptions = { - /** Dialog title to display in the header. Default depends on the dialog */ - title?: string; - /** Url of dialog icon to display in the header. Default is Platform.Bible logo */ - iconUrl?: string; - /** The message to show the user in the dialog. Default depends on the dialog */ - prompt?: string; - }; - /** Data in each tab that is a dialog. Added to DialogOptions in `dialog.service-host.ts` */ - export type DialogData = DialogOptions & { - isDialog: true; - }; -} -declare module 'renderer/components/dialogs/dialog-base.data' { - import { FloatSize, TabLoader, TabSaver } from 'shared/models/docking-framework.model'; - import { DialogData } from 'shared/models/dialog-options.model'; - import { ReactElement } from 'react'; - /** Base type for DialogDefinition. Contains reasonable defaults for dialogs */ - export type DialogDefinitionBase = Readonly<{ - /** Overwritten in {@link DialogDefinition}. Must be specified by all DialogDefinitions */ - tabType?: string; - /** Overwritten in {@link DialogDefinition}. Must be specified by all DialogDefinitions */ - Component?: (props: DialogProps) => ReactElement; - /** - * The default icon for this dialog. This may be overridden by the `DialogOptions.iconUrl` - * - * Defaults to the Platform.Bible logo - */ - defaultIconUrl?: string; - /** - * The default title for this dialog. This may be overridden by the `DialogOptions.title` - * - * Defaults to the DialogDefinition's `tabType` - */ - defaultTitle?: string; - /** The width and height at which the dialog will be loaded in CSS `px` units */ - initialSize: FloatSize; - /** The minimum width to which the dialog can be set in CSS `px` units */ - minWidth?: number; - /** The minimum height to which the dialog can be set in CSS `px` units */ - minHeight?: number; - /** - * The function used to load the dialog into the dock layout. Default uses the `Component` field - * and passes in the `DialogProps` - */ - loadDialog: TabLoader; - /** - * The function used to save the dialog into the dock layout - * - * Default does not save the dialog as they cannot properly be restored yet. - * - * TODO: preserve requests between refreshes - save the dialog info in such a way that it works - * when loading again after refresh - */ - saveDialog: TabSaver; - }>; - /** Props provided to the dialog component */ - export type DialogProps = DialogData & { - /** - * Sends the data as a resolved response to the dialog request and closes the dialog - * - * @param data Data with which to resolve the request - */ - submitDialog(data: TData): void; - /** Cancels the dialog request (resolves the response with `undefined`) and closes the dialog */ - cancelDialog(): void; - /** - * Rejects the dialog request with the specified message and closes the dialog - * - * @param errorMessage Message to explain why the dialog request was rejected - */ - rejectDialog(errorMessage: string): void; - }; - /** - * Set the functionality of submitting and canceling dialogs. This should be called specifically by - * `dialog.service-host.ts` immediately on startup and by nothing else. This is only here to - * mitigate a dependency cycle - * - * @param dialogServiceFunctions Functions from the dialog service host for resolving and rejecting - * dialogs - */ - export function hookUpDialogService({ - resolveDialogRequest: resolve, - rejectDialogRequest: reject, - }: { - resolveDialogRequest: (id: string, data: unknown | undefined) => void; - rejectDialogRequest: (id: string, message: string) => void; - }): void; - /** - * Static definition of a dialog that can be shown in Platform.Bible - * - * For good defaults, dialogs can include all the properties of this dialog. Dialogs must then - * specify `tabType` and `Component` in order to comply with `DialogDefinition` - * - * Note: this is not a class that can be inherited because all properties would be static but then - * we would not be able to use the default `loadDialog` because it would be using a static reference - * to a nonexistent `Component`. Instead of inheriting this as a class, any dialog definition can - * spread this `{ ...DIALOG_BASE }` - */ - const DIALOG_BASE: DialogDefinitionBase; - export default DIALOG_BASE; -} -declare module 'renderer/components/dialogs/dialog-definition.model' { - import { DialogOptions } from 'shared/models/dialog-options.model'; - import { DialogDefinitionBase, DialogProps } from 'renderer/components/dialogs/dialog-base.data'; - import { ReactElement } from 'react'; - /** The tabType for the select project dialog in `select-project.dialog.tsx` */ - export const SELECT_PROJECT_DIALOG_TYPE = 'platform.selectProject'; - /** The tabType for the select multiple projects dialog in `select-multiple-projects.dialog.tsx` */ - export const SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE = 'platform.selectMultipleProjects'; - /** Options to provide when showing the Select Project dialog */ - export type SelectProjectDialogOptions = DialogOptions & { - /** Project IDs to exclude from showing in the dialog */ - excludeProjectIds?: string[]; - }; - /** Options to provide when showing the Select Multiple Project dialog */ - export type SelectMultipleProjectsDialogOptions = DialogOptions & { - /** Project IDs to exclude from showing in the dialog */ - excludeProjectIds?: string[]; - /** Project IDs that should start selected in the dialog */ - selectedProjectIds?: string[]; - }; - /** - * Mapped type for dialog functions to use in getting various types for dialogs - * - * Keys should be dialog names, and values should be {@link DialogDataTypes} - * - * If you add a dialog here, you must also add it on {@link DIALOGS} - */ - export interface DialogTypes { - [SELECT_PROJECT_DIALOG_TYPE]: DialogDataTypes; - [SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE]: DialogDataTypes< - SelectMultipleProjectsDialogOptions, - string[] - >; - } - /** Each type of dialog. These are the tab types used in the dock layout */ - export type DialogTabTypes = keyof DialogTypes; - /** Types related to a specific dialog */ - export type DialogDataTypes = { - /** - * The dialog options to specify when calling the dialog. Passed into `loadDialog` as - * SavedTabInfo.data - * - * The default implementation of `loadDialog` passes all the options down to the dialog component - * as props - */ - options: TOptions; - /** The type of the response to the dialog request */ - responseType: TReturnType; - /** Props provided to the dialog component */ - props: DialogProps & TOptions; - }; - export type DialogDefinition = Readonly< - DialogDefinitionBase & { - /** - * Type of tab - indicates what kind of built-in dock layout tab this dialog definition - * represents - */ - tabType: DialogTabType; - /** - * React component to render for this dialog - * - * This must be specified only if you do not overwrite the default `loadDialog` - * - * @param props Props that will be passed through from the dialog tab's data - * @returns React element to render - */ - Component: ( - props: DialogProps & - DialogTypes[DialogTabType]['options'], - ) => ReactElement; +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 { 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"; + /** + * 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 + * 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 + * following to its `.d.ts` file: + * + * @example + * + * ```typescript + * declare module 'papi-shared-types' { + * export interface CommandHandlers { + * 'myExtension.myCommand1': (foo: string, bar: number) => string; + * 'myExtension.myCommand2': (foo: string) => Promise; + * } + * } + * ``` + */ + interface CommandHandlers { + 'test.echo': (message: string) => string; + 'test.echoRenderer': (message: string) => Promise; + 'test.echoExtensionHost': (message: string) => Promise; + 'test.throwError': (message: string) => void; + 'platform.restartExtensionHost': () => Promise; + 'platform.quit': () => Promise; + 'test.addMany': (...nums: number[]) => number; + 'test.throwErrorExtensionHost': (message: string) => void; } - >; -} -declare module 'shared/services/dialog.service-model' { - import { DialogTabTypes, DialogTypes } from 'renderer/components/dialogs/dialog-definition.model'; - import { DialogOptions } from 'shared/models/dialog-options.model'; - /** - * - * Prompt the user for responses with dialogs - */ - export interface DialogService { - /** - * Shows a dialog to the user and prompts the user to respond - * - * @type `TReturn` - The type of data the dialog responds with - * @param dialogType The type of dialog to show the user - * @param options Various options for configuring the dialog that shows - * @returns Returns the user's response or `undefined` if the user cancels - */ - showDialog( - dialogType: DialogTabType, - options?: DialogTypes[DialogTabType]['options'], - ): Promise; - /** - * Shows a select project dialog to the user and prompts the user to select a dialog - * - * @param options Various options for configuring the dialog that shows - * @returns Returns the user's selected project id or `undefined` if the user cancels - */ - selectProject(options?: DialogOptions): Promise; - } - /** Prefix on requests that indicates that the request is related to dialog operations */ - export const CATEGORY_DIALOG = 'dialog'; -} -declare module 'shared/services/dialog.service' { - import { DialogService } from 'shared/services/dialog.service-model'; - const dialogService: DialogService; - export default dialogService; -} -declare module 'extension-host/extension-types/extension-activation-context.model' { - import { ExecutionToken } from 'node/models/execution-token.model'; - import { UnsubscriberAsyncList } from 'platform-bible-utils'; - /** An object of this type is passed into `activate()` for each extension during initialization */ - export type ExecutionActivationContext = { - /** Canonical name of the extension */ - name: string; - /** Used to save and load data from the storage service. */ - executionToken: ExecutionToken; - /** Tracks all registrations made by an extension so they can be cleaned up when it is unloaded */ - registrations: UnsubscriberAsyncList; - }; -} -declare module 'renderer/hooks/papi-hooks/use-dialog-callback.hook' { - import { DialogTabTypes, DialogTypes } from 'renderer/components/dialogs/dialog-definition.model'; - export type UseDialogCallbackOptions = { - /** - * How many dialogs are allowed to be open at once from this dialog callback. Calling the callback - * when this number of maximum open dialogs has been reached does nothing. Set to -1 for - * unlimited. Defaults to 1. - */ - maximumOpenDialogs?: number; - }; - /** - * - * Enables using `papi.dialogs.showDialog` in React more easily. Returns a callback to run that will - * open a dialog with the provided `dialogType` and `options` then run the `resolveCallback` with - * the dialog response or `rejectCallback` if there is an error. By default, only one dialog can be - * open at a time. - * - * If you need to open multiple dialogs and track which dialog is which, you can set - * `options.shouldOpenMultipleDialogs` to `true` and add a counter to the `options` when calling the - * callback. Then `resolveCallback` will be resolved with that options object including your - * counter. - * - * @type `DialogTabType` The dialog type you are using. Should be inferred by parameters - * @param dialogType Dialog type you want to show on the screen - * - * 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 updating this parameter will not cause a new - * callback to be returned. However, because of the nature of calling dialogs, this has no adverse - * effect on the functionality of this hook. Calling the callback will always use the latest - * `dialogType`. - * @param options Various options for configuring the dialog that shows and this hook. If an - * `options` parameter is also provided to the returned `showDialog` callback, those - * callback-provided `options` merge over these hook-provided `options` - * - * 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 updating this parameter will not cause a new - * callback to be returned. However, because of the nature of calling dialogs, this has no adverse - * effect on the functionality of this hook. Calling the callback will always use the latest - * `options`. - * @param resolveCallback `(response, dialogType, options)` The function that will be called if the - * dialog request resolves properly - * - * - `response` - the resolved value of the dialog call. Either the user's response or `undefined` if - * the user cancels - * - `dialogType` - the value of `dialogType` at the time that this dialog was called - * - `options` the `options` provided to the dialog at the time that this dialog was called. This - * consists of the `options` provided to the returned `showDialog` callback merged over the - * `options` provided to the hook and additionally contains {@link UseDialogCallbackOptions} - * properties - * - * 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 updating this parameter will not cause a new - * callback to be returned. However, because of the nature of calling dialogs, this has no adverse - * effect on the functionality of this hook. When the dialog resolves, it will always call the - * latest `resolveCallback`. - * @param rejectCallback `(error, dialogType, options)` The function that will be called if the - * dialog request throws an error - * - * - `error` - the error thrown while calling the dialog - * - `dialogType` - the value of `dialogType` at the time that this dialog was called - * - `options` the `options` provided to the dialog at the time that this dialog was called. This - * consists of the `options` provided to the returned `showDialog` callback merged over the - * `options` provided to the hook and additionally contains {@link UseDialogCallbackOptions} - * properties - * - * 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 updating this parameter will not cause a new - * callback to be returned. However, because of the nature of calling dialogs, this has no adverse - * effect on the functionality of this hook. If the dialog throws an error, it will always call - * the latest `rejectCallback`. - * @returns `showDialog(options?)` - callback to run to show the dialog to prompt the user for a - * response - * - * - `optionsOverrides?` - `options` object you may specify that will merge over the `options` you - * provide to the hook before passing to the dialog. All properties are optional, so you may - * specify as many or as few properties here as you want to overwrite the properties in the - * `options` you provide to the hook - */ - function useDialogCallback< - DialogTabType extends DialogTabTypes, - DialogOptions extends DialogTypes[DialogTabType]['options'], - >( - dialogType: DialogTabType, - options: DialogOptions & UseDialogCallbackOptions, - resolveCallback: ( - response: DialogTypes[DialogTabType]['responseType'] | undefined, - dialogType: DialogTabType, - options: DialogOptions, - ) => void, - rejectCallback: (error: unknown, dialogType: DialogTabType, options: DialogOptions) => void, - ): (optionOverrides?: Partial) => Promise; - /** - * - * Enables using `papi.dialogs.showDialog` in React more easily. Returns a callback to run that will - * open a dialog with the provided `dialogType` and `options` then run the `resolveCallback` with - * the dialog response or `rejectCallback` if there is an error. By default, only one dialog can be - * open at a time. - * - * If you need to open multiple dialogs and track which dialog is which, you can set - * `options.shouldOpenMultipleDialogs` to `true` and add a counter to the `options` when calling the - * callback. Then `resolveCallback` will be resolved with that options object including your - * counter. - * - * @type `DialogTabType` The dialog type you are using. Should be inferred by parameters - * @param dialogType Dialog type you want to show on the screen - * - * 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 updating this parameter will not cause a new - * callback to be returned. However, because of the nature of calling dialogs, this has no adverse - * effect on the functionality of this hook. Calling the callback will always use the latest - * `dialogType`. - * @param options Various options for configuring the dialog that shows and this hook. If an - * `options` parameter is also provided to the returned `showDialog` callback, those - * callback-provided `options` merge over these hook-provided `options` - * - * 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 updating this parameter will not cause a new - * callback to be returned. However, because of the nature of calling dialogs, this has no adverse - * effect on the functionality of this hook. Calling the callback will always use the latest - * `options`. - * @param resolveCallback `(response, dialogType, options)` The function that will be called if the - * dialog request resolves properly - * - * - `response` - the resolved value of the dialog call. Either the user's response or `undefined` if - * the user cancels - * - `dialogType` - the value of `dialogType` at the time that this dialog was called - * - `options` the `options` provided to the dialog at the time that this dialog was called. This - * consists of the `options` provided to the returned `showDialog` callback merged over the - * `options` provided to the hook and additionally contains {@link UseDialogCallbackOptions} - * properties - * - * 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 updating this parameter will not cause a new - * callback to be returned. However, because of the nature of calling dialogs, this has no adverse - * effect on the functionality of this hook. When the dialog resolves, it will always call the - * latest `resolveCallback`. - * @param rejectCallback `(error, dialogType, options)` The function that will be called if the - * dialog request throws an error - * - * - `error` - the error thrown while calling the dialog - * - `dialogType` - the value of `dialogType` at the time that this dialog was called - * - `options` the `options` provided to the dialog at the time that this dialog was called. This - * consists of the `options` provided to the returned `showDialog` callback merged over the - * `options` provided to the hook and additionally contains {@link UseDialogCallbackOptions} - * properties - * - * 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 updating this parameter will not cause a new - * callback to be returned. However, because of the nature of calling dialogs, this has no adverse - * effect on the functionality of this hook. If the dialog throws an error, it will always call - * the latest `rejectCallback`. - * @returns `showDialog(options?)` - callback to run to show the dialog to prompt the user for a - * response - * - * - `optionsOverrides?` - `options` object you may specify that will merge over the `options` you - * provide to the hook before passing to the dialog. All properties are optional, so you may - * specify as many or as few properties here as you want to overwrite the properties in the - * `options` you provide to the hook - */ - function useDialogCallback< - DialogTabType extends DialogTabTypes, - DialogOptions extends DialogTypes[DialogTabType]['options'], - >( - dialogType: DialogTabType, - options: DialogOptions & UseDialogCallbackOptions, - resolveCallback: ( - response: DialogTypes[DialogTabType]['responseType'] | undefined, - dialogType: DialogTabType, - options: DialogOptions, - ) => void, - ): (optionOverrides?: Partial) => Promise; - export default useDialogCallback; -} -declare module 'shared/services/settings.service' { - import { Unsubscriber } from 'platform-bible-utils'; - import { SettingNames, SettingTypes } from 'papi-shared-types'; - /** Event to set or update a setting */ - export type UpdateSettingEvent = { - type: 'update-setting'; - setting: SettingTypes[SettingName]; - }; - /** Event to remove a setting */ - export type ResetSettingEvent = { - type: 'reset-setting'; - }; - /** All supported setting events */ - export type SettingEvent = - | UpdateSettingEvent - | ResetSettingEvent; - /** - * Retrieves the value of the specified setting - * - * @param key The string id of the setting for which the value is being retrieved - * @param defaultSetting The default value used for the setting if no value is available for the key - * @returns The value of the specified setting, parsed to an object. Returns default setting if - * setting does not exist - */ - const getSetting: ( - key: SettingName, - defaultSetting: SettingTypes[SettingName], - ) => SettingTypes[SettingName]; - /** - * Sets the value of the specified setting - * - * @param key The string id of the setting for which the value is being retrieved - * @param newSetting The value that is to be stored. Setting the new value to `undefined` is the - * equivalent of deleting the setting - */ - const setSetting: ( - key: SettingName, - newSetting: SettingTypes[SettingName], - ) => void; - /** - * Removes the setting from memory - * - * @param key The string id of the setting for which the value is being removed - */ - const resetSetting: (key: SettingName) => void; - /** - * Subscribes to updates of the specified setting. Whenever the value of the setting changes, the - * callback function is executed. - * - * @param key The string id of the setting for which the value is being subscribed to - * @param callback The function that will be called whenever the specified setting is updated - * @returns Unsubscriber that should be called whenever the subscription should be deleted - */ - const subscribeToSetting: ( - key: SettingName, - callback: (newSetting: SettingEvent) => void, - ) => Unsubscriber; - export interface SettingsService { - get: typeof getSetting; - set: typeof setSetting; - reset: typeof resetSetting; - subscribe: typeof subscribeToSetting; - } - /** - * - * Service that allows to get and set settings in local storage - */ - const settingsService: SettingsService; - export default settingsService; -} -declare module '@papi/core' { - /** Exporting empty object so people don't have to put 'type' in their import statements */ - const core: {}; - export default core; - export type { ExecutionActivationContext } from 'extension-host/extension-types/extension-activation-context.model'; - export type { ExecutionToken } from 'node/models/execution-token.model'; - export type { DialogTypes } from 'renderer/components/dialogs/dialog-definition.model'; - export type { UseDialogCallbackOptions } from 'renderer/hooks/papi-hooks/use-dialog-callback.hook'; - export type { default as IDataProvider } from 'shared/models/data-provider.interface'; - export type { - DataProviderUpdateInstructions, - DataProviderDataType, - DataProviderSubscriberOptions, - } from 'shared/models/data-provider.model'; - export type { WithNotifyUpdate } from 'shared/models/data-provider-engine.model'; - export type { default as IDataProviderEngine } from 'shared/models/data-provider-engine.model'; - export type { DialogOptions } from 'shared/models/dialog-options.model'; - export type { - ExtensionDataScope, - MandatoryProjectDataType, - } from 'shared/models/project-data-provider.model'; - export type { ProjectMetadata } from 'shared/models/project-metadata.model'; - export type { - GetWebViewOptions, - SavedWebViewDefinition, - UseWebViewStateHook, - WebViewContentType, - WebViewDefinition, - WebViewProps, - } from 'shared/models/web-view.model'; - export type { IWebViewProvider } from 'shared/models/web-view-provider.model'; - export type { SettingEvent } from 'shared/services/settings.service'; -} -declare module 'shared/services/menu-data.service-model' { - import { - OnDidDispose, - UnsubscriberAsync, - MultiColumnMenu, - ReferencedItem, - WebViewMenu, - } from 'platform-bible-utils'; - import { - DataProviderDataType, - DataProviderSubscriberOptions, - DataProviderUpdateInstructions, - } from 'shared/models/data-provider.model'; - import { IDataProvider } from '@papi/core'; - /** - * - * This name is used to register the menu data data provider on the papi. You can use this name to - * find the data provider when accessing it using the useData hook - */ - export const menuDataServiceProviderName = 'platform.menuDataServiceDataProvider'; - export const menuDataServiceObjectToProxy: Readonly<{ - /** - * - * This name is used to register the menu data data provider on the papi. You can use this name to - * find the data provider when accessing it using the useData hook - */ - dataProviderName: 'platform.menuDataServiceDataProvider'; - }>; - export type MenuDataDataTypes = { - MainMenu: DataProviderDataType; - WebViewMenu: DataProviderDataType; - }; - module 'papi-shared-types' { - interface DataProviders { - [menuDataServiceProviderName]: IMenuDataService; + /** + * Names for each command available on the papi. + * + * Automatically includes all extensions' commands that are added to {@link CommandHandlers}. + * + * @example 'platform.quit'; + */ + type CommandNames = keyof CommandHandlers; + interface SettingTypes { + 'platform.verseRef': ScriptureReference; + placeholder: undefined; } - } - /** - * - * Service that allows to get and store menu data - */ - export type IMenuDataService = { + type SettingNames = keyof SettingTypes; + /** This is just a simple example so we have more than one. It's not intended to be real. */ + type NotesOnlyProjectDataTypes = MandatoryProjectDataType & { + 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. + * + * 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. + * + * 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): * - * Get menu content for the main menu + * @example * - * @param mainMenuType Does not have to be defined - * @returns MultiColumnMenu object of main menu content + * ```typescript + * declare module 'papi-shared-types' { + * export type MyProjectDataType = MandatoryProjectDataType & { + * MyProjectData: DataProviderDataType; + * }; + * + * export interface ProjectDataProviders { + * MyExtensionProjectTypeName: IDataProvider; + * } + * } + * ``` */ - getMainMenu(mainMenuType: undefined): Promise; + interface ProjectDataProviders { + 'platform.notesOnly': IDataProvider; + 'platform.placeholder': IDataProvider; + } /** + * Names for each project data provider available on the papi. * - * Get menu content for the main menu + * Automatically includes all extensions' project data providers that are added to + * {@link ProjectDataProviders}. * - * @param mainMenuType Does not have to be defined - * @returns MultiColumnMenu object of main menu content + * @example 'platform.placeholder' */ - getMainMenu(): Promise; + type ProjectTypes = keyof ProjectDataProviders; /** - * This data cannot be changed. Trying to use this setter this will always throw + * `DataProviderDataTypes` for each project data provider supported by PAPI. These are the data + * types served by each project data provider. + * + * Automatically includes all extensions' project data providers that are added to + * {@link ProjectDataProviders}. * - * @param mainMenuType Does not have to be defined - * @param value MultiColumnMenu object to set as the main menu - * @returns Unsubscriber function + * @example + * + * ```typescript + * ProjectDataTypes['MyExtensionProjectTypeName'] => { + * MyProjectData: DataProviderDataType; + * } + * ``` */ - setMainMenu( - mainMenuType: undefined, - value: never, - ): Promise>; + type ProjectDataTypes = { + [ProjectType in ProjectTypes]: ExtractDataProviderDataTypes; + }; + type StuffDataTypes = { + Stuff: DataProviderDataType; + }; + type PlaceholderDataTypes = { + Placeholder: DataProviderDataType<{ + thing: number; + }, string[], number>; + }; /** - * Subscribe to run a callback function when the main menu data is changed + * `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`. * - * @param mainMenuType Does not have to be defined - * @param callback Function to run with the updated menuContent for this selector - * @param options Various options to adjust how the subscriber emits updates - * @returns Unsubscriber function (run to unsubscribe from listening for updates) + * Note: 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. + * + * An extension can extend this interface to add types for the data provider it registers by + * adding the following to its `.d.ts` file (in this example, we are adding the + * `'helloSomeone.people'` data provider types): + * + * @example + * + * ```typescript + * declare module 'papi-shared-types' { + * export type PeopleDataTypes = { + * Greeting: DataProviderDataType; + * Age: DataProviderDataType; + * People: DataProviderDataType; + * }; + * + * export type PeopleDataMethods = { + * deletePerson(name: string): Promise; + * testRandomMethod(things: string): Promise; + * }; + * + * export type PeopleDataProvider = IDataProvider & PeopleDataMethods; + * + * export interface DataProviders { + * 'helloSomeone.people': PeopleDataProvider; + * } + * } + * ``` */ - subscribeMainMenu( - mainMenuType: undefined, - callback: (menuContent: MultiColumnMenu) => void, - options?: DataProviderSubscriberOptions, - ): Promise; + interface DataProviders { + 'platform.stuff': IDataProvider; + 'platform.placeholder': IDataProvider; + } /** - * Get menu content for a web view + * Names for each data provider available on the papi. + * + * Automatically includes all extensions' data providers that are added to {@link DataProviders}. * - * @param webViewType The type of webview for which a menu should be retrieved - * @returns WebViewMenu object of web view menu content + * @example 'platform.placeholder' */ - getWebViewMenu(webViewType: ReferencedItem): Promise; + type DataProviderNames = keyof DataProviders; /** - * This data cannot be changed. Trying to use this setter this will always throw + * `DataProviderDataTypes` for each data provider supported by PAPI. These are the data types + * served by each data provider. + * + * Automatically includes all extensions' data providers that are added to {@link DataProviders}. + * + * @example * - * @param webViewType The type of webview for which a menu should be set - * @param value Menu of specified webViewType - * @returns Unsubscriber function + * ```typescript + * DataProviderTypes['helloSomeone.people'] => { + * Greeting: DataProviderDataType; + * Age: DataProviderDataType; + * People: DataProviderDataType; + * } + * ``` */ - setWebViewMenu( - webViewType: ReferencedItem, - value: never, - ): Promise>; + type DataProviderTypes = { + [DataProviderName in DataProviderNames]: ExtractDataProviderDataTypes; + }; /** - * Subscribe to run a callback function when the web view menu data is changed + * Disposable version of each data provider type supported by PAPI. These objects are only + * returned from `papi.dataProviders.registerEngine` - only the one who registers a data provider + * engine is allowed to dispose of the data provider. * - * @param webViewType The type of webview for which a menu should be subscribed - * @param callback Function to run with the updated menuContent for this selector - * @param options Various options to adjust how the subscriber emits updates - * @returns Unsubscriber function (run to unsubscribe from listening for updates) - */ - subscribeWebViewMenu( - webViewType: ReferencedItem, - callback: (menuContent: WebViewMenu) => void, - options?: DataProviderSubscriberOptions, - ): Promise; - } & OnDidDispose & - typeof menuDataServiceObjectToProxy & - IDataProvider; -} -declare module 'shared/services/menu-data.service' { - import { IMenuDataService } from 'shared/services/menu-data.service-model'; - const menuDataService: IMenuDataService; - export default menuDataService; + * Automatically includes all extensions' data providers that are added to {@link DataProviders}. + */ + type DisposableDataProviders = { + [DataProviderName in DataProviderNames]: IDisposableDataProvider; + }; } -declare module '@papi/backend' { - /** - * Unified module for accessing API features in the extension host. - * - * WARNING: DO NOT IMPORT papi IN ANY FILE THAT papi IMPORTS AND EXPOSES. - */ - import * as commandService from 'shared/services/command.service'; - import { PapiNetworkService } from 'shared/services/network.service'; - 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 { 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'; - const papi: { +declare module "shared/services/command.service" { + import { UnsubscriberAsync } from 'platform-bible-utils'; + import { CommandHandlers, CommandNames } from 'papi-shared-types'; + module 'papi-shared-types' { + interface CommandHandlers { + 'test.addThree': typeof addThree; + 'test.squareAndConcat': typeof squareAndConcat; + } + } + function addThree(a: number, b: number, c: number): Promise; + function squareAndConcat(a: number, b: string): Promise; + /** Sets up the CommandService. Only runs once and always returns the same promise after that */ + export const initialize: () => Promise; + /** Send a command to the backend. */ + export const sendCommand: (commandName: CommandName, ...args: Parameters) => Promise>>; /** + * Creates a function that is a command function with a baked commandName. This is also nice because + * you get TypeScript type support using this function. * - * 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`. + * @param commandName Command name for command function + * @returns Function to call with arguments of command that sends the command and resolves with the + * result of the command + */ + export const createSendCommandFunction: (commandName: CommandName) => (...args: Parameters) => Promise>>; + /** + * Register a command on the papi to be handled here * - * @see IDataProviderEngine for more information on extending this class. + * @param commandName Command name to register for handling here + * + * - Note: Command 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. + * + * @param handler Function to run when the command is invoked + * @returns True if successfully registered, throws with error message if not */ - DataProviderEngine: typeof PapiDataProviderEngine; - /** This is just an alias for internet.fetch */ - fetch: typeof globalThis.fetch; + export const registerCommand: (commandName: CommandName, handler: CommandHandlers[CommandName]) => Promise; /** + * JSDOC SOURCE commandService * * The command service allows you to exchange messages with other components in the platform. You * can register a command that other services and extensions can send you. You can send commands to * other services and extensions that have registered commands. */ - commands: typeof commandService; + export type moduleSummaryComments = {}; +} +declare module "shared/models/docking-framework.model" { + import { MutableRefObject, ReactNode } from 'react'; + import { DockLayout, DropDirection, LayoutBase } from 'rc-dock'; + import { SavedWebViewDefinition, WebViewDefinition, WebViewDefinitionUpdateInfo } from "shared/models/web-view.model"; /** + * Saved information used to recreate a tab. * - * Service exposing various functions related to using webViews - * - * WebViews are iframes in the Platform.Bible UI into which extensions load frontend code, either - * HTML or React components. + * - {@link TabLoader} loads this into {@link TabInfo} + * - {@link TabSaver} saves {@link TabInfo} into this */ - webViews: WebViewServiceType; + export type SavedTabInfo = { + /** + * Tab ID - a unique identifier that identifies this tab. If this tab is a WebView, this ID will + * match the `WebViewDefinition.id` + */ + id: string; + /** Type of tab - indicates what kind of built-in tab this info represents */ + tabType: string; + /** Data needed to load the tab */ + data?: unknown; + }; /** - * - * Interface for registering webView providers - */ - webViewProviders: PapiWebViewProviderService; + * Information that Paranext uses to create a tab in the dock layout. + * + * - {@link TabLoader} loads {@link SavedTabInfo} into this + * - {@link TabSaver} saves this into {@link SavedTabInfo} + */ + export type TabInfo = SavedTabInfo & { + /** + * Url of image to show on the title bar of the tab + * + * Defaults to Platform.Bible logo + */ + tabIconUrl?: string; + /** Text to show on the title bar of the tab */ + tabTitle: string; + /** Text to show when hovering over the title bar of the tab */ + tabTooltip?: string; + /** Content to show inside the tab. */ + content: ReactNode; + /** (optional) Minimum width that the tab can become in CSS `px` units */ + minWidth?: number; + /** (optional) Minimum height that the tab can become in CSS `px` units */ + minHeight?: number; + }; /** + * Function that takes a {@link SavedTabInfo} and creates a Paranext tab out of it. Each type of tab + * must provide a {@link TabLoader}. * - * Prompt the user for responses with dialogs + * For now all tab creators must do their own data type verification */ - dialogs: DialogService; + export type TabLoader = (savedTabInfo: SavedTabInfo) => TabInfo; /** + * Function that takes a Paranext tab and creates a saved tab out of it. Each type of tab can + * provide a {@link TabSaver}. If they do not provide one, the properties added by `TabInfo` are + * stripped from TabInfo by `saveTabInfoBase` before saving (so it is just a {@link SavedTabInfo}). * - * Service that provides a way to send and receive network events + * @param tabInfo The Paranext tab to save + * @returns The saved tab info for Paranext to persist. If `undefined`, does not save the tab */ - network: PapiNetworkService; + export type TabSaver = (tabInfo: TabInfo) => SavedTabInfo | undefined; + /** Information about a tab in a panel */ + interface TabLayout { + type: 'tab'; + } /** + * Indicates where to display a floating window * - * All extensions and services should use this logger to provide a unified output of logs + * - `cascade` - place the window a bit below and to the right of the previously created floating + * window + * - `center` - center the window in the dock layout */ - logger: import('electron-log').MainLogger & { - default: import('electron-log').MainLogger; + type FloatPosition = 'cascade' | 'center'; + /** The dimensions for a floating tab in CSS `px` units */ + export type FloatSize = { + width: number; + height: number; }; + /** Information about a floating window */ + export interface FloatLayout { + type: 'float'; + floatSize?: FloatSize; + /** Where to display the floating window. Defaults to `cascade` */ + position?: FloatPosition; + } + export type PanelDirection = 'left' | 'right' | 'bottom' | 'top' | 'before-tab' | 'after-tab' | 'maximize' | 'move' | 'active' | 'update'; + /** Information about a panel */ + interface PanelLayout { + type: 'panel'; + direction?: PanelDirection; + /** If undefined, it will add in the `direction` relative to the previously added tab. */ + targetTabId?: string; + } + /** Information about how a Paranext tab fits into the dock layout */ + export type Layout = TabLayout | FloatLayout | PanelLayout; + /** Event emitted when webViews are created */ + export type AddWebViewEvent = { + webView: SavedWebViewDefinition; + layout: Layout; + }; + /** Props that are passed to the web view tab component */ + export type WebViewTabProps = WebViewDefinition; + /** Rc-dock's onLayoutChange prop made asynchronous - resolves */ + export type OnLayoutChangeRCDock = (newLayout: LayoutBase, currentTabId?: string, direction?: DropDirection) => Promise; + /** Properties related to the dock layout */ + export type PapiDockLayout = { + /** The rc-dock dock layout React element ref. Used to perform operations on the layout */ + dockLayout: DockLayout; + /** + * A ref to a function that runs when the layout changes. We set this ref to our + * {@link onLayoutChange} function + */ + onLayoutChangeRef: MutableRefObject; + /** + * Add or update a tab in the layout + * + * @param savedTabInfo Info for tab to add or update + * @param layout Information about where to put a new tab + * @returns If tab added, final layout used to display the new tab. If existing tab updated, + * `undefined` + */ + addTabToDock: (savedTabInfo: SavedTabInfo, layout: Layout) => Layout | undefined; + /** + * Add or update a webview in the layout + * + * @param webView Web view to add or update + * @param layout Information about where to put a new webview + * @returns If WebView added, final layout used to display the new webView. If existing webView + * updated, `undefined` + */ + addWebViewToDock: (webView: WebViewTabProps, layout: Layout) => Layout | undefined; + /** + * Remove a tab in the layout + * + * @param tabId ID of the tab to remove + */ + removeTabFromDock: (tabId: string) => boolean; + /** + * Gets the WebView definition for the web view with the specified ID + * + * @param webViewId The ID of the WebView whose web view definition to get + * @returns WebView definition with the specified ID or undefined if not found + */ + getWebViewDefinition: (webViewId: string) => WebViewDefinition | undefined; + /** + * Updates the WebView with the specified ID with the specified properties + * + * @param webViewId The ID of the WebView to update + * @param updateInfo Properties to update on the WebView. Any unspecified properties will stay the + * same + * @returns True if successfully found the WebView to update; false otherwise + */ + updateWebViewDefinition: (webViewId: string, updateInfo: WebViewDefinitionUpdateInfo) => boolean; + /** + * The layout to use as the default layout if the dockLayout doesn't have a layout loaded. + * + * TODO: This should be removed and the `testLayout` imported directly in this file once this + * service is refactored to split the code between processes. The only reason this is passed from + * `platform-dock-layout.component.tsx` is that we cannot import `testLayout` here since this + * service is currently all shared code. Refactor should happen in #203 + */ + testLayout: LayoutBase; + }; +} +declare module "shared/services/web-view.service-model" { + import { GetWebViewOptions, WebViewId, WebViewType } from "shared/models/web-view.model"; + import { AddWebViewEvent, Layout } from "shared/models/docking-framework.model"; + import { PlatformEvent } from 'platform-bible-utils'; /** + * JSDOC SOURCE papiWebViewService * - * Service that provides a way to call `fetch` since the original function is not available + * Service exposing various functions related to using webViews + * + * WebViews are iframes in the Platform.Bible UI into which extensions load frontend code, either + * HTML or React components. */ - internet: InternetService; + export interface WebViewServiceType { + /** Event that emits with webView info when a webView is added */ + onDidAddWebView: PlatformEvent; + /** + * Creates a new web view or gets an existing one depending on if you request an existing one and + * if the web view provider decides to give that existing one to you (it is up to the provider). + * + * @param webViewType Type of WebView to create + * @param layout Information about where you want the web view to go. Defaults to adding as a tab + * @param options Options that affect what this function does. For example, you can provide an + * existing web view ID to request an existing web view with that ID. + * @returns Promise that resolves to the ID of the webview we got or undefined if the provider did + * not create a WebView for this request. + * @throws If something went wrong like the provider for the webViewType was not found + */ + getWebView: (webViewType: WebViewType, layout?: Layout, options?: GetWebViewOptions) => Promise; + } + /** Name to use when creating a network event that is fired when webViews are created */ + export const EVENT_NAME_ON_DID_ADD_WEB_VIEW: `${string}:${string}`; + export const NETWORK_OBJECT_NAME_WEB_VIEW_SERVICE = "WebViewService"; +} +declare module "shared/services/network-object-status.service-model" { + import { NetworkObjectDetails } from "shared/models/network-object.model"; + export interface NetworkObjectStatusRemoteServiceType { + /** + * Get details about all available network objects + * + * @returns Object whose keys are the names of the network objects and whose values are the + * {@link NetworkObjectDetails} for each network object + */ + getAllNetworkObjectDetails: () => Promise>; + } + /** Provides functions related to the set of available network objects */ + export interface NetworkObjectStatusServiceType extends NetworkObjectStatusRemoteServiceType { + /** + * Get a promise that resolves when a network object is registered or rejects if a timeout is hit + * + * @returns Promise that either resolves to the {@link NetworkObjectDetails} for a network object + * once the network object is registered, or rejects if a timeout is provided and the timeout is + * reached before the network object is registered + */ + waitForNetworkObject: (id: string, timeoutInMS?: number) => Promise; + } + export const networkObjectStatusServiceNetworkObjectName = "NetworkObjectStatusService"; +} +declare module "shared/services/network-object-status.service" { + import { NetworkObjectStatusServiceType } from "shared/services/network-object-status.service-model"; + const networkObjectStatusService: NetworkObjectStatusServiceType; + export default networkObjectStatusService; +} +declare module "shared/services/web-view.service" { + import { WebViewServiceType } from "shared/services/web-view.service-model"; + const webViewService: WebViewServiceType; + export default webViewService; +} +declare module "shared/models/web-view-provider.model" { + import { GetWebViewOptions, WebViewDefinition, SavedWebViewDefinition } from "shared/models/web-view.model"; + import { DisposableNetworkObject, NetworkObject, NetworkableObject } from "shared/models/network-object.model"; + import { CanHaveOnDidDispose } from 'platform-bible-utils'; + export interface IWebViewProvider extends NetworkableObject { + /** + * @param savedWebView Filled out if an existing webview is being called for (matched by ID). Just + * ID if this is a new request or if the web view with the existing ID was not found + * @param getWebViewOptions + */ + getWebView(savedWebView: SavedWebViewDefinition, getWebViewOptions: GetWebViewOptions): Promise; + } + export interface WebViewProvider extends NetworkObject, CanHaveOnDidDispose { + } + export interface DisposableWebViewProvider extends DisposableNetworkObject, Omit { + } +} +declare module "shared/services/web-view-provider.service" { /** - * - * Service that allows extensions to send and receive data to/from other extensions + * Handles registering web view providers and serving web views around the papi. Exposed on the + * papi. */ - dataProviders: DataProviderService; + import { DisposableWebViewProvider, IWebViewProvider, WebViewProvider } from "shared/models/web-view-provider.model"; + /** Sets up the service. Only runs once and always returns the same promise after that */ + const initialize: () => Promise; /** + * Indicate if we are aware of an existing web view provider with the given type. If a web view + * provider with the given type 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. * - * Service that registers and gets project data providers + * @param webViewType Type of webView to check for */ - projectDataProviders: PapiBackendProjectDataProviderService; + function hasKnown(webViewType: string): boolean; /** + * Register a web view provider to serve webViews for a specified type of webViews * - * Provides metadata for projects known by the platform + * @param webViewType Type of web view to provide + * @param webViewProvider Object to register as a webView provider including control over disposing + * of it. + * + * WARNING: setting a webView provider mutates the provided object. + * @returns `webViewProvider` modified to be a network object */ - projectLookup: ProjectLookupServiceType; + function register(webViewType: string, webViewProvider: IWebViewProvider): Promise; /** + * Get a web view provider that has previously been set up * - * This service provides extensions in the extension host the ability to read/write data based on - * the extension identity and current user (as identified by the OS). This service will not work - * within the renderer. + * @param webViewType Type of webview provider to get + * @returns Web view provider with the given name if one exists, undefined otherwise */ - storage: ExtensionStorageService; + function get(webViewType: string): Promise; + export interface WebViewProviderService { + initialize: typeof initialize; + hasKnown: typeof hasKnown; + register: typeof register; + get: typeof get; + } + export interface PapiWebViewProviderService { + register: typeof register; + } + const webViewProviderService: WebViewProviderService; /** + * JSDOC SOURCE papiWebViewProviderService * - * Service that allows to get and store menu data + * Interface for registering webView providers */ - menuData: IMenuDataService; - }; - export default papi; - /** - * - * 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 const DataProviderEngine: typeof PapiDataProviderEngine; - /** This is just an alias for internet.fetch */ - export const fetch: typeof globalThis.fetch; - /** - * - * The command service allows you to exchange messages with other components in the platform. You - * can register a command that other services and extensions can send you. You can send commands to - * other services and extensions that have registered commands. - */ - export const commands: typeof commandService; - /** - * - * Service exposing various functions related to using webViews - * - * WebViews are iframes in the Platform.Bible UI into which extensions load frontend code, either - * HTML or React components. - */ - export const webViews: WebViewServiceType; - /** - * - * Interface for registering webView providers - */ - export const webViewProviders: PapiWebViewProviderService; - /** - * - * Prompt the user for responses with dialogs - */ - export const dialogs: DialogService; - /** - * - * Service that provides a way to send and receive network events - */ - export const network: PapiNetworkService; - /** - * - * All extensions and services should use this logger to provide a unified output of logs - */ - export const logger: import('electron-log').MainLogger & { - default: import('electron-log').MainLogger; - }; - /** - * - * Service that provides a way to call `fetch` since the original function is not available - */ - export const internet: InternetService; - /** - * - * Service that allows extensions to send and receive data to/from other extensions - */ - export const dataProviders: DataProviderService; - /** - * - * Service that registers and gets project data providers - */ - export const projectDataProviders: PapiBackendProjectDataProviderService; - /** - * - * Provides metadata for projects known by the platform - */ - export const projectLookup: ProjectLookupServiceType; - /** - * - * This service provides extensions in the extension host the ability to read/write data based on - * the extension identity and current user (as identified by the OS). This service will not work - * within the renderer. - */ - export const storage: ExtensionStorageService; - /** - * - * Service that allows to get and store menu data - */ - export const menuData: IMenuDataService; + export const papiWebViewProviderService: PapiWebViewProviderService; + export default webViewProviderService; } -declare module 'extension-host/extension-types/extension.interface' { - import { UnsubscriberAsync } from 'platform-bible-utils'; - import { ExecutionActivationContext } from 'extension-host/extension-types/extension-activation-context.model'; - /** Interface for all extensions to implement */ - export interface IExtension { +declare module "shared/services/internet.service" { + /** Our shim over fetch. Allows us to control internet access. */ + const papiFetch: typeof fetch; + export interface InternetService { + fetch: typeof papiFetch; + } /** - * Sets up this extension! Runs when paranext wants this extension to activate. For example, - * activate() should register commands for this extension + * JSDOC SOURCE internetService * - * @param context Data and utilities that are specific to this particular extension + * Service that provides a way to call `fetch` since the original function is not available */ - activate: (context: ExecutionActivationContext) => Promise; + const internetService: InternetService; + export default internetService; +} +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 } from "shared/models/data-provider-engine.model"; + import { DataProviderNames, DataProviderTypes, DataProviders, DisposableDataProviders } from 'papi-shared-types'; + import IDataProvider, { IDisposableDataProvider } from "shared/models/data-provider.interface"; /** - * Deactivate anything in this extension that is not covered by the registrations in the context - * object given to activate(). + * JSDOC SOURCE DataProviderEngine * - * @returns Promise that resolves to true if successfully deactivated + * 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. */ - deactivate?: UnsubscriberAsync; - } -} -declare module 'extension-host/extension-types/extension-manifest.model' { - /** Information about an extension provided by the extension developer. */ - export type ExtensionManifest = { - /** Name of the extension */ - name: string; + 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; /** - * Extension version - expected to be [semver](https://semver.org/) like `"0.1.3"`. + * 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 * - * Note: semver may become a hard requirement in the future, so we recommend using it now. + * @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 */ - version: string; + function ignore(method: Function & { + isIgnored?: boolean; + }): void; /** - * Path to the JavaScript file to run in the extension host. Relative to the extension's root - * folder. + * 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 * - * Must be specified. Can be an empty string if the extension does not have any JavaScript to run. + * @param target The class that has the method to ignore + * @param member The name of the method to ignore + * + * 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. */ - main: string; + function ignore(target: T, member: keyof T): void; /** - * Path to the TypeScript type declaration file that describes this extension and its interactions - * on the PAPI. Relative to the extension's root folder. + * A collection of decorators to be used with the data provider service * - * If not provided, Platform.Bible will look in the following locations: + * @example To use the `ignore` a decorator on a class's method: * - * 1. `.d.ts` - * 2. `.d.ts` - * 3. `index.d.ts` + * ```typescript + * class MyDataProviderEngine { + * @papi.dataProviders.decorators.ignore + * async getInternal() {} + * } + * ``` * - * See [Extension Anatomy - Type Declaration - * Files](https://github.com/paranext/paranext-extension-template/wiki/Extension-Anatomy#type-declaration-files-dts) - * for more information about extension type declaration files. + * 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. */ - types?: string; + const decorators: { + ignore: typeof ignore; + }; + /** + * 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. + */ + function registerEngine(providerName: DataProviderName, dataProviderEngine: IDataProviderEngine, dataProviderType?: string, dataProviderAttributes?: { + [property: string]: unknown; + } | undefined): Promise; + /** + * Creates a data provider to be shared on the network layering over the provided data provider + * engine. + * + * @type `TDataTypes` - The data provider data types served by the data provider to create. + * + * This is not exposed on the papi as it is a helper function to enable other services to layer over + * this service and create their own subsets of data providers with other types than + * `DataProviders` types using this function and {@link getByType} + * @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. + */ + export function registerEngineByType(providerName: string, dataProviderEngine: IDataProviderEngine, dataProviderType?: string, dataProviderAttributes?: { + [property: string]: unknown; + } | undefined): Promise>>; + /** + * 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 + */ + function get(providerName: DataProviderName): Promise; + /** + * Get a data provider that has previously been set up + * + * @type `T` - The type of data provider to get. Use `IDataProvider`, + * specifying your own types, or provide a custom data provider type + * + * This is not exposed on the papi as it is a helper function to enable other services to layer over + * this service and create their own subsets of data providers with other types than + * `DataProviders` types using this function and {@link registerEngineByType} + * @param providerName Name of the desired data provider + * @returns The data provider with the given name if one exists, undefined otherwise + */ + export function getByType>(providerName: string): Promise; + export interface DataProviderService { + hasKnown: typeof hasKnown; + registerEngine: typeof registerEngine; + get: typeof get; + decorators: typeof decorators; + DataProviderEngine: typeof DataProviderEngine; + } /** - * List of events that occur that should cause this extension to be activated. Not yet - * implemented. + * JSDOC SOURCE dataProviderService + * + * Service that allows extensions to send and receive data to/from other extensions */ - activationEvents: string[]; - }; -} -declare module 'renderer/hooks/hook-generators/create-use-network-object-hook.util' { - import { NetworkObject } from 'shared/models/network-object.model'; - /** - * This function takes in a getNetworkObject function and creates a hook with that function in it - * which will return a network object - * - * @param getNetworkObject A function that takes in an id string and returns a network object - * @param mapParametersToNetworkObjectSource Function that takes the parameters passed into the hook - * and returns the `networkObjectSource` associated with those parameters. Defaults to taking the - * first parameter passed into the hook and using that as the `networkObjectSource`. - * - * - Note: `networkObjectSource` is string name of the network object to get OR `networkObject` - * (result of this hook, if you want this hook to just return the network object again) - * - * @returns A function that takes in a networkObjectSource and returns a NetworkObject - */ - function createUseNetworkObjectHook( - getNetworkObject: (...args: THookParams) => Promise | undefined>, - mapParametersToNetworkObjectSource?: ( - ...args: THookParams - ) => string | NetworkObject | undefined, - ): (...args: THookParams) => NetworkObject | undefined; - export default createUseNetworkObjectHook; + const dataProviderService: DataProviderService; + export default dataProviderService; } -declare module 'renderer/hooks/papi-hooks/use-data-provider.hook' { - import { DataProviders } from 'papi-shared-types'; - /** - * Gets a data provider with specified provider name - * - * @type `T` - The type of data provider to return. Use `IDataProvider`, - * specifying your own types, or provide a custom data provider type - * @param dataProviderSource String name of the data provider to get OR dataProvider (result of - * useDataProvider, if you want this hook to just return the data provider again) - * @returns Undefined if the data provider has not been retrieved, data provider if it has been - * retrieved and is not disposed, and undefined again if the data provider is disposed - */ - const useDataProvider: ( - dataProviderSource: DataProviderName | DataProviders[DataProviderName] | undefined, - ) => DataProviders[DataProviderName] | undefined; - export default useDataProvider; -} -declare module 'renderer/hooks/hook-generators/create-use-data-hook.util' { - import { - DataProviderSubscriberOptions, - DataProviderUpdateInstructions, - } from 'shared/models/data-provider.model'; - import IDataProvider from 'shared/models/data-provider.interface'; - import ExtractDataProviderDataTypes from 'shared/models/extract-data-provider-data-types.model'; - /** - * The final function called as part of the `useData` hook that is the actual React hook - * - * This is the `.Greeting(...)` part of `useData('helloSomeone.people').Greeting(...)` - */ - type UseDataFunctionWithProviderType< - TDataProvider extends IDataProvider, - TDataType extends keyof ExtractDataProviderDataTypes, - > = ( - selector: ExtractDataProviderDataTypes[TDataType]['selector'], - defaultValue: ExtractDataProviderDataTypes[TDataType]['getData'], - subscriberOptions?: DataProviderSubscriberOptions, - ) => [ - ExtractDataProviderDataTypes[TDataType]['getData'], - ( - | (( - newData: ExtractDataProviderDataTypes[TDataType]['setData'], - ) => Promise>>) - | undefined - ), - boolean, - ]; - /** - * A proxy that serves the actual hooks for a single data provider - * - * This is the `useData('helloSomeone.people')` part of - * `useData('helloSomeone.people').Greeting(...)` - */ - type UseDataProxy> = { - [TDataType in keyof ExtractDataProviderDataTypes]: UseDataFunctionWithProviderType< - TDataProvider, - TDataType - >; - }; - /** - * React hook to use data provider data with various data types - * - * @example `useData('helloSomeone.people').Greeting('Bill', 'Greeting loading')` - * - * @type `TDataProvider` - The type of data provider to get. Use - * `IDataProvider`, specifying your own types, or provide a custom data - * provider type - */ - type UseDataHookGeneric = { - >( - ...args: TUseDataProviderParams - ): UseDataProxy; - }; - /** - * Create a `useData(...).DataType(selector, defaultValue, options)` hook for a specific subset of - * data providers as supported by `useDataProviderHook` - * - * @param useDataProviderHook Hook that gets a data provider from a specific subset of data - * providers - * @returns `useData` hook for getting data from a data provider - */ - function createUseDataHook( - useDataProviderHook: (...args: TUseDataProviderParams) => IDataProvider | undefined, - ): UseDataHookGeneric; - export default createUseDataHook; +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 */ + export type ProjectDataProviderEngineTypes = { + [ProjectType in ProjectTypes]: IDataProviderEngine; + }; + export interface ProjectDataProviderEngineFactory { + createProjectDataProviderEngine(projectId: string, projectStorageInterpreterId: string): ProjectDataProviderEngineTypes[ProjectType]; + } } -declare module 'renderer/hooks/papi-hooks/use-data.hook' { - import { - DataProviderSubscriberOptions, - DataProviderUpdateInstructions, - } from 'shared/models/data-provider.model'; - import { DataProviderNames, DataProviderTypes, DataProviders } from 'papi-shared-types'; - /** - * React hook to use data from a data provider - * - * @example `useData('helloSomeone.people').Greeting('Bill', 'Greeting loading')` - */ - type UseDataHook = { - ( - dataProviderSource: DataProviderName | DataProviders[DataProviderName] | undefined, - ): { - [TDataType in keyof DataProviderTypes[DataProviderName]]: ( - // @ts-ignore TypeScript pretends it can't find `selector`, but it works just fine - selector: DataProviderTypes[DataProviderName][TDataType]['selector'], - // @ts-ignore TypeScript pretends it can't find `getData`, but it works just fine - defaultValue: DataProviderTypes[DataProviderName][TDataType]['getData'], - subscriberOptions?: DataProviderSubscriberOptions, - ) => [ - // @ts-ignore TypeScript pretends it can't find `getData`, but it works just fine - DataProviderTypes[DataProviderName][TDataType]['getData'], - ( - | (( - // @ts-ignore TypeScript pretends it can't find `setData`, but it works just fine - newData: DataProviderTypes[DataProviderName][TDataType]['setData'], - ) => Promise>) - | undefined - ), - boolean, - ]; +declare module "shared/models/project-metadata.model" { + import { ProjectTypes } from 'papi-shared-types'; + /** + * Low-level information describing a project that Platform.Bible directly manages and uses to load + * project data + */ + export type ProjectMetadata = { + /** ID of the project (must be unique and case insensitive) */ + id: string; + /** Short name of the project (not necessarily unique) */ + name: string; + /** Indicates how the project is persisted to storage */ + storageType: string; + /** + * Indicates what sort of project this is which implies its data shape (e.g., what data streams + * should be available) + */ + projectType: ProjectTypes; }; - }; - /** - * ```typescript - * useData( - * dataProviderSource: DataProviderName | DataProviders[DataProviderName] | undefined, - * ).DataType( - * selector: DataProviderTypes[DataProviderName][DataType]['selector'], - * defaultValue: DataProviderTypes[DataProviderName][DataType]['getData'], - * subscriberOptions?: DataProviderSubscriberOptions, - * ) => [ - * DataProviderTypes[DataProviderName][DataType]['getData'], - * ( - * | (( - * newData: DataProviderTypes[DataProviderName][DataType]['setData'], - * ) => Promise>) - * | undefined - * ), - * boolean, - * ] - * ``` - * - * React hook to use data from a data provider. Subscribes to run a callback on a data provider's - * data with specified selector on the specified data type that data provider serves. - * - * Usage: Specify the data provider and the data type on the data provider with - * `useData('').` and use like any other React hook. - * - * _@example_ Subscribing to Verse data at JHN 11:35 on the `'quickVerse.quickVerse'` data provider: - * - * ```typescript - * const [verseText, setVerseText, verseTextIsLoading] = useData('quickVerse.quickVerse').Verse( - * 'JHN 11:35', - * 'Verse text goes here', - * ); - * ``` - * - * _@param_ `dataProviderSource` string name of data provider to get OR dataProvider (result of - * useDataProvider if you want to consolidate and only get the data provider once) - * - * _@param_ `selector` tells the provider what data this listener is listening for - * - * 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 data - * - * _@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_ `[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 - * - `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 - * 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 - * provider - */ - const useData: UseDataHook; - export default useData; } -declare module 'renderer/hooks/papi-hooks/use-setting.hook' { - import { SettingTypes } from 'papi-shared-types'; - /** - * Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes - * and gets updated when the setting is changed by others. - * - * @param key The string id that is used to store the setting in local storage - * - * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be - * updated every render - * @param defaultState The default state of the setting. If the setting already has a value set to - * it in local storage, this parameter will be ignored. - * - * Note: this parameter is internally assigned to a `ref`, so changing it will not cause any hooks - * to re-run with its new value. Running `resetSetting()` will always update the setting value - * returned to the latest `defaultState`, and changing the `key` will use the latest - * `defaultState`. However, if `defaultState` is changed while a setting is `defaultState` - * (meaning it is reset and has no value), the returned setting value will not be updated to the - * new `defaultState`. - * @returns `[setting, setSetting, resetSetting]` - * - * - `setting`: The current state of the setting, either `defaultState` or the stored state on the - * papi, if any - * - `setSetting`: Function that updates the setting to a new value - * - `resetSetting`: Function that removes the setting and resets the value to `defaultState` - * - * @throws When subscription callback function is called with an update that has an unexpected - * message type - */ - const useSetting: ( - key: SettingName, - defaultState: SettingTypes[SettingName], - ) => [ - setting: SettingTypes[SettingName], - setSetting: (newSetting: SettingTypes[SettingName]) => void, - resetSetting: () => void, - ]; - export default useSetting; +declare module "shared/services/project-lookup.service-model" { + import { ProjectMetadata } from "shared/models/project-metadata.model"; + /** + * JSDOC SOURCE projectLookupService + * + * Provides metadata for projects known by the platform + */ + export interface ProjectLookupServiceType { + /** + * Provide metadata for all projects found on the local system + * + * @returns ProjectMetadata for all projects stored on the local system + */ + getMetadataForAllProjects: () => Promise; + /** + * Look up metadata for a specific project ID + * + * @param projectId ID of the project to load + * @returns ProjectMetadata from the 'meta.json' file for the given project + */ + getMetadataForProject: (projectId: string) => Promise; + } + export const projectLookupServiceNetworkObjectName = "ProjectLookupService"; } -declare module 'renderer/hooks/papi-hooks/use-project-data-provider.hook' { - import { ProjectDataProviders } from 'papi-shared-types'; - /** - * 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 - * 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 - */ - const useProjectDataProvider: ( - projectType: ProjectType, - projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined, - ) => ProjectDataProviders[ProjectType] | undefined; - export default useProjectDataProvider; +declare module "shared/services/project-lookup.service" { + import { ProjectLookupServiceType } from "shared/services/project-lookup.service-model"; + const projectLookupService: ProjectLookupServiceType; + export default projectLookupService; } -declare module 'renderer/hooks/papi-hooks/use-project-data.hook' { - import { - DataProviderSubscriberOptions, - DataProviderUpdateInstructions, - } from 'shared/models/data-provider.model'; - import { ProjectDataProviders, ProjectDataTypes, ProjectTypes } from 'papi-shared-types'; - /** - * React hook to use data from a project data provider - * - * @example `useProjectData('ParatextStandard', 'project id').VerseUSFM(...);` - */ - type UseProjectDataHook = { - ( - projectType: ProjectType, - projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined, - ): { - [TDataType in keyof ProjectDataTypes[ProjectType]]: ( - // @ts-ignore TypeScript pretends it can't find `selector`, but it works just fine - selector: ProjectDataTypes[ProjectType][TDataType]['selector'], - // @ts-ignore TypeScript pretends it can't find `getData`, but it works just fine - defaultValue: ProjectDataTypes[ProjectType][TDataType]['getData'], - subscriberOptions?: DataProviderSubscriberOptions, - ) => [ - // @ts-ignore TypeScript pretends it can't find `getData`, but it works just fine - ProjectDataTypes[ProjectType][TDataType]['getData'], - ( - | (( - // @ts-ignore TypeScript pretends it can't find `setData`, but it works just fine - newData: ProjectDataTypes[ProjectType][TDataType]['setData'], - ) => Promise>) - | undefined - ), - boolean, - ]; +declare module "shared/services/project-data-provider.service" { + import { ProjectTypes, ProjectDataProviders } from 'papi-shared-types'; + import { ProjectDataProviderEngineFactory } from "shared/models/project-data-provider-engine.model"; + import { Dispose } from 'platform-bible-utils'; + /** + * Add a new Project Data Provider Factory to PAPI that uses the given engine. There must not be an + * existing factory already that handles the same project type or this operation will fail. + * + * @param projectType Type of project that pdpEngineFactory supports + * @param pdpEngineFactory Used in a ProjectDataProviderFactory to create ProjectDataProviders + * @returns Promise that resolves to a disposable object when the registration operation completes + */ + export function registerProjectDataProviderEngineFactory(projectType: ProjectType, pdpEngineFactory: ProjectDataProviderEngineFactory): Promise; + /** + * Get a Project Data Provider for the given project ID. + * + * @example + * + * ```typescript + * const pdp = await get('ParatextStandard', 'ProjectID12345'); + * pdp.getVerse(new VerseRef('JHN', '1', '1')); + * ``` + * + * @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 + * project's actual `projectType` (according to its metadata), a warning will be logged + * @param projectId ID for the project to load + * @returns Data provider with types that are associated with the given project type + */ + export function get(projectType: ProjectType, projectId: string): Promise; + export interface PapiBackendProjectDataProviderService { + registerProjectDataProviderEngineFactory: typeof registerProjectDataProviderEngineFactory; + get: typeof get; + } + /** + * JSDOC SOURCE papiBackendProjectDataProviderService + * + * Service that registers and gets project data providers + */ + export const papiBackendProjectDataProviderService: PapiBackendProjectDataProviderService; + export interface PapiFrontendProjectDataProviderService { + get: typeof get; + } + /** + * JSDOC SOURCE papiFrontendProjectDataProviderService + * + * Service that gets project data providers + */ + export const papiFrontendProjectDataProviderService: { + get: typeof get; }; - }; - /** - * ```typescript - * useProjectData( - * projectType: ProjectType, - * projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined, - * ).DataType( - * selector: ProjectDataTypes[ProjectType][DataType]['selector'], - * defaultValue: ProjectDataTypes[ProjectType][DataType]['getData'], - * subscriberOptions?: DataProviderSubscriberOptions, - * ) => [ - * ProjectDataTypes[ProjectType][DataType]['getData'], - * ( - * | (( - * newData: ProjectDataTypes[ProjectType][DataType]['setData'], - * ) => Promise>) - * | undefined - * ), - * boolean, - * ] - * ``` - * - * 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 - * with `useProjectData('', '').` and use like any other React - * hook. - * - * _@example_ Subscribing to Verse USFM info at JHN 11:35 on a `ParatextStandard` project with - * projectId `32664dc3288a28df2e2bb75ded887fc8f17a15fb`: - * - * ```typescript - * const [verse, setVerse, verseIsLoading] = useProjectData( - * 'ParatextStandard', - * '32664dc3288a28df2e2bb75ded887fc8f17a15fb', - * ).VerseUSFM( - * useMemo(() => new VerseRef('JHN', '11', '35', ScrVers.English), []), - * 'Loading verse ', - * ); - * ``` - * - * _@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 - * 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) - * - * _@param_ `selector` tells the provider what data this listener is listening for - * - * 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 data - * - * _@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 - * until `projectDataProviderSource` or `selector` changes. - * - * _@returns_ `[data, setData, isLoading]` - */ - const useProjectData: UseProjectDataHook; - export default useProjectData; -} -declare module 'renderer/hooks/papi-hooks/use-data-provider-multi.hook' { - import { DataProviderNames, DataProviders } from 'papi-shared-types'; - /** - * Gets an array of data providers based on an array of input sources - * - * @type `T` - The types of data providers to return. Use `IDataProvider`, - * specifying your own types, or provide a custom data provider type for each item in the array. - * Note that if you provide more than one data type, each item in the returned array will be - * considered to be any of those types. For example, if you call `useDataProviderMulti`, all items in the returned array will be considered to be of type `Type1 | Type2 | - * undefined`. Although you can determine the actual type based on the array index, TypeScript - * will not know, so you will need to type assert the array items for later type checking to - * work. - * @param dataProviderSources Array containing string names of the data providers to get OR data - * providers themselves (i.e., the results of useDataProvider/useDataProviderMulti) if you want - * this hook to return the data providers again. It is fine to have a mix of strings and data - * providers in the array. - * - * WARNING: THE ARRAY MUST BE STABLE - const or wrapped in useState, useMemo, etc. It must not be - * updated every render. - * @returns An array of data providers that correspond by index to the values in - * `dataProviderSources`. Each item in the array will be (a) undefined if the data provider has - * not been retrieved or has been disposed, or (b) a data provider if it has been retrieved and is - * not disposed. - */ - function useDataProviderMulti( - dataProviderSources: ( - | EachDataProviderName[number] - | DataProviders[EachDataProviderName[number]] - | undefined - )[], - ): (DataProviders[EachDataProviderName[number]] | undefined)[]; - export default useDataProviderMulti; -} -declare module 'renderer/hooks/papi-hooks/index' { - export { default as useDataProvider } from 'renderer/hooks/papi-hooks/use-data-provider.hook'; - 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 useDialogCallback } from 'renderer/hooks/papi-hooks/use-dialog-callback.hook'; - export { default as useDataProviderMulti } from 'renderer/hooks/papi-hooks/use-data-provider-multi.hook'; } -declare module '@papi/frontend/react' { - export * from 'renderer/hooks/papi-hooks/index'; +declare module "shared/data/file-system.model" { + /** Types to use with file system operations */ + /** + * Represents a path in file system or other. Has a scheme followed by :// followed by a relative + * path. If no scheme is provided, the app scheme is used. Available schemes are as follows: + * + * - `app://` - goes to the app's home directory and into `.platform.bible` (platform-dependent) + * - `cache://` - goes to the app's temporary file cache at `app://cache` + * - `data://` - goes to the app's data storage location at `app://data` + * - `resources://` - goes to the resources directory installed in the app + * - `file://` - an absolute file path from root + */ + export type Uri = string; } -declare module 'renderer/services/renderer-xml-http-request.service' { - /** This wraps the browser's XMLHttpRequest implementation to - * provide better control over internet access. It is isomorphic with the standard XMLHttpRequest, - * so it should act as a drop-in replacement. - * - * Note that Node doesn't have a native implementation, so this is only for the renderer. - */ - export default class PapiRendererXMLHttpRequest implements XMLHttpRequest { - readonly DONE: 4; - readonly HEADERS_RECEIVED: 2; - readonly LOADING: 3; - readonly OPENED: 1; - readonly UNSENT: 0; - abort: () => void; - addEventListener: ( - type: K, - listener: (this: XMLHttpRequest, ev: XMLHttpRequestEventMap[K]) => any, - options?: boolean | AddEventListenerOptions, - ) => void; - dispatchEvent: (event: Event) => boolean; - getAllResponseHeaders: () => string; - getResponseHeader: (name: string) => string | null; - open: ( - method: string, - url: string, - async?: boolean, - username?: string | null, - password?: string | null, - ) => void; - onabort: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - onerror: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - onload: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - onloadend: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - onloadstart: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - onprogress: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - onreadystatechange: ((this: XMLHttpRequest, ev: Event) => any) | null; - ontimeout: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - overrideMimeType: (mime: string) => void; - readyState: number; - removeEventListener: ( - type: K, - listener: (this: XMLHttpRequest, ev: XMLHttpRequestEventMap[K]) => any, - options?: boolean | EventListenerOptions, - ) => void; - response: any; - responseText: string; - responseType: XMLHttpRequestResponseType; - responseURL: string; - responseXML: Document | null; - send: (body?: Document | XMLHttpRequestBodyInit | null) => void; - setRequestHeader: (name: string, value: string) => void; - status: number; - statusText: string; - timeout: number; - upload: XMLHttpRequestUpload; - withCredentials: boolean; - constructor(); - } +declare module "node/utils/util" { + import { Uri } from "shared/data/file-system.model"; + export const FILE_PROTOCOL = "file://"; + export const RESOURCES_PROTOCOL = "resources://"; + export function resolveHtmlPath(htmlFileName: string): string; + /** + * Gets the platform-specific user Platform.Bible folder for this application + * + * When running in development: `/dev-appdata` + * + * When packaged: `/.platform.bible` + */ + export const getAppDir: import("memoize-one").MemoizedFn<() => string>; + /** + * Resolves the uri to a path + * + * @param uri The uri to resolve + * @returns Real path to the uri supplied + */ + export function getPathFromUri(uri: Uri): string; + /** + * Combines the uri passed in with the paths passed in to make one uri + * + * @param uri Uri to start from + * @param paths Paths to combine into the uri + * @returns One uri that combines the uri and the paths in left-to-right order + */ + export function joinUriPaths(uri: Uri, ...paths: string[]): Uri; } -declare module '@papi/frontend' { - /** - * Unified module for accessing API features in the renderer. - * - * WARNING: DO NOT IMPORT papi IN ANY FILE THAT papi IMPORTS AND EXPOSES. - */ - import * as commandService from 'shared/services/command.service'; - import { PapiNetworkService } from 'shared/services/network.service'; - import { WebViewServiceType } from 'shared/services/web-view.service-model'; - import { InternetService } from 'shared/services/internet.service'; - import { DataProviderService } from 'shared/services/data-provider.service'; - import { ProjectLookupServiceType } from 'shared/services/project-lookup.service-model'; - import { PapiFrontendProjectDataProviderService } from 'shared/services/project-data-provider.service'; - import { SettingsService } from 'shared/services/settings.service'; - import { DialogService } from 'shared/services/dialog.service-model'; - import * as papiReact from '@papi/frontend/react'; - import PapiRendererWebSocket from 'renderer/services/renderer-web-socket.service'; - import { IMenuDataService } from 'shared/services/menu-data.service-model'; - import PapiRendererXMLHttpRequest from 'renderer/services/renderer-xml-http-request.service'; - const papi: { - /** This is just an alias for internet.fetch */ - fetch: typeof globalThis.fetch; - /** This wraps the browser's WebSocket implementation to provide - * better control over internet access. It is isomorphic with the standard WebSocket, so it should - * act as a drop-in replacement. +declare module "node/services/node-file-system.service" { + /** File system calls from Node */ + import fs, { BigIntStats } from 'fs'; + import { Uri } from "shared/data/file-system.model"; + /** + * Read a text file * - * Note that the Node WebSocket implementation is different and not wrapped here. + * @param uri URI of file + * @returns Promise that resolves to the contents of the file */ - WebSocket: typeof PapiRendererWebSocket; - /** This wraps the browser's XMLHttpRequest implementation to - * provide better control over internet access. It is isomorphic with the standard XMLHttpRequest, - * so it should act as a drop-in replacement. + export function readFileText(uri: Uri): Promise; + /** + * Read a binary file * - * Note that Node doesn't have a native implementation, so this is only for the renderer. + * @param uri URI of file + * @returns Promise that resolves to the contents of the file */ - XMLHttpRequest: typeof PapiRendererXMLHttpRequest; + export function readFileBinary(uri: Uri): Promise; /** + * Write data to a file * - * The command service allows you to exchange messages with other components in the platform. You - * can register a command that other services and extensions can send you. You can send commands to - * other services and extensions that have registered commands. + * @param uri URI of file + * @param fileContents String or Buffer to write into the file + * @returns Promise that resolves after writing the file */ - commands: typeof commandService; + export function writeFile(uri: Uri, fileContents: string | Buffer): Promise; /** + * Copies a file from one location to another. Creates the path to the destination if it does not + * exist * - * Service exposing various functions related to using webViews + * @param sourceUri The location of the file to copy + * @param destinationUri The uri to the file to create as a copy of the source file + * @param mode Bitwise modifiers that affect how the copy works. See + * [`fsPromises.copyFile`](https://nodejs.org/api/fs.html#fspromisescopyfilesrc-dest-mode) for + * more information + */ + export function copyFile(sourceUri: Uri, destinationUri: Uri, mode?: Parameters[2]): Promise; + /** + * Delete a file if it exists * - * WebViews are iframes in the Platform.Bible UI into which extensions load frontend code, either - * HTML or React components. + * @param uri URI of file + * @returns Promise that resolves when the file is deleted or determined to not exist */ - webViews: WebViewServiceType; + export function deleteFile(uri: Uri): Promise; /** + * Get stats about the file or directory. Note that BigInts are used instead of ints to avoid. + * https://en.wikipedia.org/wiki/Year_2038_problem * - * Prompt the user for responses with dialogs + * @param uri URI of file or directory + * @returns Promise that resolves to object of type https://nodejs.org/api/fs.html#class-fsstats if + * file or directory exists, undefined if it doesn't */ - dialogs: DialogService; + export function getStats(uri: Uri): Promise; /** + * Set the last modified and accessed times for the file or directory * - * Service that provides a way to send and receive network events + * @param uri URI of file or directory + * @returns Promise that resolves once the touch operation finishes */ - network: PapiNetworkService; + export function touch(uri: Uri, date: Date): Promise; + /** Type of file system item in a directory */ + export enum EntryType { + File = "file", + Directory = "directory", + Unknown = "unknown" + } + /** All entries in a directory, mapped from entry type to array of uris for the entries */ + export type DirectoryEntries = Readonly<{ + [entryType in EntryType]: Uri[]; + }>; /** + * Reads a directory and returns lists of entries in the directory by entry type. * - * All extensions and services should use this logger to provide a unified output of logs + * @param uri - URI of directory. + * @param entryFilter - Function to filter out entries in the directory based on their names. + * @returns Map of entry type to list of uris for each entry in the directory with that type. + */ + export function readDir(uri: Uri, entryFilter?: (entryName: string) => boolean): Promise; + /** + * Create a directory in the file system if it does not exist. Does not throw if it already exists. + * + * @param uri URI of directory + * @returns Promise that resolves once the directory has been created */ - logger: import('electron-log').MainLogger & { - default: import('electron-log').MainLogger; + export function createDir(uri: Uri): Promise; + /** + * Remove a directory and all its contents recursively from the file system + * + * @param uri URI of directory + * @returns Promise that resolves when the delete operation finishes + */ + export function deleteDir(uri: Uri): Promise; +} +declare module "node/utils/crypto-util" { + export function createUuid(): string; + /** + * Create a cryptographically secure nonce that is at least 128 bits long. See nonce spec at + * https://w3c.github.io/webappsec-csp/#security-nonces + * + * @param encoding: "base64url" (HTML safe, shorter string) or "hex" (longer string) From + * https://base64.guru/standards/base64url, the purpose of this encoding is "the ability to use + * the encoding result as filename or URL address" + * @param numberOfBytes: Number of bytes the resulting nonce should contain + * @returns Cryptographically secure, pseudo-randomly generated value encoded as a string + */ + export function createNonce(encoding: 'base64url' | 'hex', numberOfBytes?: number): string; +} +declare module "node/models/execution-token.model" { + /** For now this is just for extensions, but maybe we will want to expand this in the future */ + export type ExecutionTokenType = 'extension'; + /** Execution tokens can be passed into API calls to provide context about their identity */ + export class ExecutionToken { + readonly type: ExecutionTokenType; + readonly name: string; + readonly nonce: string; + constructor(tokenType: ExecutionTokenType, name: string); + getHash(): string; + } +} +declare module "node/services/execution-token.service" { + import { ExecutionToken } from "node/models/execution-token.model"; + /** + * This should be called when extensions are being loaded + * + * @param extensionName Name of the extension to register + * @returns Token that can be passed to `tokenIsValid` to authenticate or authorize API callers. It + * is important that the token is not shared to avoid impersonation of API callers. + */ + function registerExtension(extensionName: string): ExecutionToken; + /** + * Remove a registered token. Note that a hash of a token is what is needed to unregister, not the + * full token itself (notably not the nonce), so something can be delegated the ability to + * unregister a token without having been given the full token itself. + * + * @param extensionName Name of the extension that was originally registered + * @param tokenHash Value of `getHash()` of the token that was originally registered. + * @returns `true` if the token was successfully unregistered, `false` otherwise + */ + function unregisterExtension(extensionName: string, tokenHash: string): boolean; + /** + * This should only be needed by services that need to contextualize the response for the caller + * + * @param executionToken Token that was previously registered. + * @returns `true` if the token matches a token that was previous registered, `false` otherwise. + */ + function tokenIsValid(executionToken: ExecutionToken): boolean; + const executionTokenService: { + registerExtension: typeof registerExtension; + unregisterExtension: typeof unregisterExtension; + tokenIsValid: typeof tokenIsValid; }; + export default executionTokenService; +} +declare module "extension-host/services/extension-storage.service" { + import { ExecutionToken } from "node/models/execution-token.model"; + import { Buffer } from 'buffer'; + /** + * This is only intended to be called by the extension service. This service cannot call into the + * extension service or it causes a circular dependency. + */ + export function setExtensionUris(urisPerExtension: Map): void; + /** Return a path to the specified file within the extension's installation directory */ + export function buildExtensionPathFromName(extensionName: string, fileName: string): string; /** + * Read a text file from the the extension's installation directory * - * Service that provides a way to call `fetch` since the original function is not available + * @param token ExecutionToken provided to the extension when `activate()` was called + * @param fileName Name of the file to be read + * @returns Promise for a string with the contents of the file */ - internet: InternetService; + function readTextFileFromInstallDirectory(token: ExecutionToken, fileName: string): Promise; /** + * Read a binary file from the the extension's installation directory * - * Service that allows extensions to send and receive data to/from other extensions + * @param token ExecutionToken provided to the extension when `activate()` was called + * @param fileName Name of the file to be read + * @returns Promise for a Buffer with the contents of the file */ - dataProviders: DataProviderService; + function readBinaryFileFromInstallDirectory(token: ExecutionToken, fileName: string): Promise; /** + * Read data specific to the user (as identified by the OS) and extension (as identified by the + * ExecutionToken) * - * Service that gets project data providers + * @param token ExecutionToken provided to the extension when `activate()` was called + * @param key Unique identifier of the data + * @returns Promise for a string containing the data */ - projectDataProviders: PapiFrontendProjectDataProviderService; + function readUserData(token: ExecutionToken, key: string): Promise; /** + * Write data specific to the user (as identified by the OS) and extension (as identified by the + * ExecutionToken) * - * Provides metadata for projects known by the platform + * @param token ExecutionToken provided to the extension when `activate()` was called + * @param key Unique identifier of the data + * @param data Data to be written + * @returns Promise that will resolve if the data is written successfully */ - projectLookup: ProjectLookupServiceType; + function writeUserData(token: ExecutionToken, key: string, data: string): Promise; /** + * Delete data previously written that is specific to the user (as identified by the OS) and + * extension (as identified by the ExecutionToken) * - * React hooks that enable interacting with the `papi` in React components more easily. + * @param token ExecutionToken provided to the extension when `activate()` was called + * @param key Unique identifier of the data + * @returns Promise that will resolve if the data is deleted successfully */ - react: typeof papiReact; + function deleteUserData(token: ExecutionToken, key: string): Promise; + export interface ExtensionStorageService { + readTextFileFromInstallDirectory: typeof readTextFileFromInstallDirectory; + readBinaryFileFromInstallDirectory: typeof readBinaryFileFromInstallDirectory; + readUserData: typeof readUserData; + writeUserData: typeof writeUserData; + deleteUserData: typeof deleteUserData; + } /** + * JSDOC SOURCE extensionStorageService * - * Service that allows to get and set settings in local storage + * This service provides extensions in the extension host the ability to read/write data based on + * the extension identity and current user (as identified by the OS). This service will not work + * within the renderer. */ - settings: SettingsService; + const extensionStorageService: ExtensionStorageService; + export default extensionStorageService; +} +declare module "shared/models/dialog-options.model" { + /** General options to adjust dialogs (created from `papi.dialogs`) */ + export type DialogOptions = { + /** Dialog title to display in the header. Default depends on the dialog */ + title?: string; + /** Url of dialog icon to display in the header. Default is Platform.Bible logo */ + iconUrl?: string; + /** The message to show the user in the dialog. Default depends on the dialog */ + prompt?: string; + }; + /** Data in each tab that is a dialog. Added to DialogOptions in `dialog.service-host.ts` */ + export type DialogData = DialogOptions & { + isDialog: true; + }; +} +declare module "renderer/components/dialogs/dialog-base.data" { + import { FloatSize, TabLoader, TabSaver } from "shared/models/docking-framework.model"; + import { DialogData } from "shared/models/dialog-options.model"; + import { ReactElement } from 'react'; + /** Base type for DialogDefinition. Contains reasonable defaults for dialogs */ + export type DialogDefinitionBase = Readonly<{ + /** Overwritten in {@link DialogDefinition}. Must be specified by all DialogDefinitions */ + tabType?: string; + /** Overwritten in {@link DialogDefinition}. Must be specified by all DialogDefinitions */ + Component?: (props: DialogProps) => ReactElement; + /** + * The default icon for this dialog. This may be overridden by the `DialogOptions.iconUrl` + * + * Defaults to the Platform.Bible logo + */ + defaultIconUrl?: string; + /** + * The default title for this dialog. This may be overridden by the `DialogOptions.title` + * + * Defaults to the DialogDefinition's `tabType` + */ + defaultTitle?: string; + /** The width and height at which the dialog will be loaded in CSS `px` units */ + initialSize: FloatSize; + /** The minimum width to which the dialog can be set in CSS `px` units */ + minWidth?: number; + /** The minimum height to which the dialog can be set in CSS `px` units */ + minHeight?: number; + /** + * The function used to load the dialog into the dock layout. Default uses the `Component` field + * and passes in the `DialogProps` + */ + loadDialog: TabLoader; + /** + * The function used to save the dialog into the dock layout + * + * Default does not save the dialog as they cannot properly be restored yet. + * + * TODO: preserve requests between refreshes - save the dialog info in such a way that it works + * when loading again after refresh + */ + saveDialog: TabSaver; + }>; + /** Props provided to the dialog component */ + export type DialogProps = DialogData & { + /** + * Sends the data as a resolved response to the dialog request and closes the dialog + * + * @param data Data with which to resolve the request + */ + submitDialog(data: TData): void; + /** Cancels the dialog request (resolves the response with `undefined`) and closes the dialog */ + cancelDialog(): void; + /** + * Rejects the dialog request with the specified message and closes the dialog + * + * @param errorMessage Message to explain why the dialog request was rejected + */ + rejectDialog(errorMessage: string): void; + }; + /** + * Set the functionality of submitting and canceling dialogs. This should be called specifically by + * `dialog.service-host.ts` immediately on startup and by nothing else. This is only here to + * mitigate a dependency cycle + * + * @param dialogServiceFunctions Functions from the dialog service host for resolving and rejecting + * dialogs + */ + export function hookUpDialogService({ resolveDialogRequest: resolve, rejectDialogRequest: reject, }: { + resolveDialogRequest: (id: string, data: unknown | undefined) => void; + rejectDialogRequest: (id: string, message: string) => void; + }): void; + /** + * Static definition of a dialog that can be shown in Platform.Bible + * + * For good defaults, dialogs can include all the properties of this dialog. Dialogs must then + * specify `tabType` and `Component` in order to comply with `DialogDefinition` + * + * Note: this is not a class that can be inherited because all properties would be static but then + * we would not be able to use the default `loadDialog` because it would be using a static reference + * to a nonexistent `Component`. Instead of inheriting this as a class, any dialog definition can + * spread this `{ ...DIALOG_BASE }` + */ + const DIALOG_BASE: DialogDefinitionBase; + export default DIALOG_BASE; +} +declare module "renderer/components/dialogs/dialog-definition.model" { + import { DialogOptions } from "shared/models/dialog-options.model"; + import { DialogDefinitionBase, DialogProps } from "renderer/components/dialogs/dialog-base.data"; + import { ReactElement } from 'react'; + /** The tabType for the select project dialog in `select-project.dialog.tsx` */ + export const SELECT_PROJECT_DIALOG_TYPE = "platform.selectProject"; + /** The tabType for the select multiple projects dialog in `select-multiple-projects.dialog.tsx` */ + export const SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE = "platform.selectMultipleProjects"; + /** Options to provide when showing the Select Project dialog */ + export type SelectProjectDialogOptions = DialogOptions & { + /** Project IDs to exclude from showing in the dialog */ + excludeProjectIds?: string[]; + }; + /** Options to provide when showing the Select Multiple Project dialog */ + export type SelectMultipleProjectsDialogOptions = DialogOptions & { + /** Project IDs to exclude from showing in the dialog */ + excludeProjectIds?: string[]; + /** Project IDs that should start selected in the dialog */ + selectedProjectIds?: string[]; + }; /** + * Mapped type for dialog functions to use in getting various types for dialogs + * + * Keys should be dialog names, and values should be {@link DialogDataTypes} + * + * If you add a dialog here, you must also add it on {@link DIALOGS} + */ + export interface DialogTypes { + [SELECT_PROJECT_DIALOG_TYPE]: DialogDataTypes; + [SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE]: DialogDataTypes; + } + /** Each type of dialog. These are the tab types used in the dock layout */ + export type DialogTabTypes = keyof DialogTypes; + /** Types related to a specific dialog */ + export type DialogDataTypes = { + /** + * The dialog options to specify when calling the dialog. Passed into `loadDialog` as + * SavedTabInfo.data + * + * The default implementation of `loadDialog` passes all the options down to the dialog component + * as props + */ + options: TOptions; + /** The type of the response to the dialog request */ + responseType: TReturnType; + /** Props provided to the dialog component */ + props: DialogProps & TOptions; + }; + export type DialogDefinition = Readonly & DialogTypes[DialogTabType]['options']) => ReactElement; + }>; +} +declare module "shared/services/dialog.service-model" { + import { DialogTabTypes, DialogTypes } from "renderer/components/dialogs/dialog-definition.model"; + import { DialogOptions } from "shared/models/dialog-options.model"; + /** + * JSDOC SOURCE dialogService + * + * Prompt the user for responses with dialogs + */ + export interface DialogService { + /** + * Shows a dialog to the user and prompts the user to respond + * + * @type `TReturn` - The type of data the dialog responds with + * @param dialogType The type of dialog to show the user + * @param options Various options for configuring the dialog that shows + * @returns Returns the user's response or `undefined` if the user cancels + */ + showDialog(dialogType: DialogTabType, options?: DialogTypes[DialogTabType]['options']): Promise; + /** + * Shows a select project dialog to the user and prompts the user to select a dialog + * + * @param options Various options for configuring the dialog that shows + * @returns Returns the user's selected project id or `undefined` if the user cancels + */ + selectProject(options?: DialogOptions): Promise; + } + /** Prefix on requests that indicates that the request is related to dialog operations */ + export const CATEGORY_DIALOG = "dialog"; +} +declare module "shared/services/dialog.service" { + import { DialogService } from "shared/services/dialog.service-model"; + const dialogService: DialogService; + export default dialogService; +} +declare module "extension-host/extension-types/extension-activation-context.model" { + import { ExecutionToken } from "node/models/execution-token.model"; + import { UnsubscriberAsyncList } from 'platform-bible-utils'; + /** An object of this type is passed into `activate()` for each extension during initialization */ + export type ExecutionActivationContext = { + /** Canonical name of the extension */ + name: string; + /** Used to save and load data from the storage service. */ + executionToken: ExecutionToken; + /** Tracks all registrations made by an extension so they can be cleaned up when it is unloaded */ + registrations: UnsubscriberAsyncList; + }; +} +declare module "renderer/hooks/papi-hooks/use-dialog-callback.hook" { + import { DialogTabTypes, DialogTypes } from "renderer/components/dialogs/dialog-definition.model"; + export type UseDialogCallbackOptions = { + /** + * How many dialogs are allowed to be open at once from this dialog callback. Calling the callback + * when this number of maximum open dialogs has been reached does nothing. Set to -1 for + * unlimited. Defaults to 1. + */ + maximumOpenDialogs?: number; + }; + /** + * JSDOC SOURCE useDialogCallback + * + * Enables using `papi.dialogs.showDialog` in React more easily. Returns a callback to run that will + * open a dialog with the provided `dialogType` and `options` then run the `resolveCallback` with + * the dialog response or `rejectCallback` if there is an error. By default, only one dialog can be + * open at a time. + * + * If you need to open multiple dialogs and track which dialog is which, you can set + * `options.shouldOpenMultipleDialogs` to `true` and add a counter to the `options` when calling the + * callback. Then `resolveCallback` will be resolved with that options object including your + * counter. + * + * @type `DialogTabType` The dialog type you are using. Should be inferred by parameters + * @param dialogType Dialog type you want to show on the screen + * + * 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 updating this parameter will not cause a new + * callback to be returned. However, because of the nature of calling dialogs, this has no adverse + * effect on the functionality of this hook. Calling the callback will always use the latest + * `dialogType`. + * @param options Various options for configuring the dialog that shows and this hook. If an + * `options` parameter is also provided to the returned `showDialog` callback, those + * callback-provided `options` merge over these hook-provided `options` + * + * 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 updating this parameter will not cause a new + * callback to be returned. However, because of the nature of calling dialogs, this has no adverse + * effect on the functionality of this hook. Calling the callback will always use the latest + * `options`. + * @param resolveCallback `(response, dialogType, options)` The function that will be called if the + * dialog request resolves properly + * + * - `response` - the resolved value of the dialog call. Either the user's response or `undefined` if + * the user cancels + * - `dialogType` - the value of `dialogType` at the time that this dialog was called + * - `options` the `options` provided to the dialog at the time that this dialog was called. This + * consists of the `options` provided to the returned `showDialog` callback merged over the + * `options` provided to the hook and additionally contains {@link UseDialogCallbackOptions} + * properties + * + * 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 updating this parameter will not cause a new + * callback to be returned. However, because of the nature of calling dialogs, this has no adverse + * effect on the functionality of this hook. When the dialog resolves, it will always call the + * latest `resolveCallback`. + * @param rejectCallback `(error, dialogType, options)` The function that will be called if the + * dialog request throws an error + * + * - `error` - the error thrown while calling the dialog + * - `dialogType` - the value of `dialogType` at the time that this dialog was called + * - `options` the `options` provided to the dialog at the time that this dialog was called. This + * consists of the `options` provided to the returned `showDialog` callback merged over the + * `options` provided to the hook and additionally contains {@link UseDialogCallbackOptions} + * properties + * + * 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 updating this parameter will not cause a new + * callback to be returned. However, because of the nature of calling dialogs, this has no adverse + * effect on the functionality of this hook. If the dialog throws an error, it will always call + * the latest `rejectCallback`. + * @returns `showDialog(options?)` - callback to run to show the dialog to prompt the user for a + * response + * + * - `optionsOverrides?` - `options` object you may specify that will merge over the `options` you + * provide to the hook before passing to the dialog. All properties are optional, so you may + * specify as many or as few properties here as you want to overwrite the properties in the + * `options` you provide to the hook + */ + function useDialogCallback(dialogType: DialogTabType, options: DialogOptions & UseDialogCallbackOptions, resolveCallback: (response: DialogTypes[DialogTabType]['responseType'] | undefined, dialogType: DialogTabType, options: DialogOptions) => void, rejectCallback: (error: unknown, dialogType: DialogTabType, options: DialogOptions) => void): (optionOverrides?: Partial) => Promise; + /** JSDOC DESTINATION useDialogCallback */ + function useDialogCallback(dialogType: DialogTabType, options: DialogOptions & UseDialogCallbackOptions, resolveCallback: (response: DialogTypes[DialogTabType]['responseType'] | undefined, dialogType: DialogTabType, options: DialogOptions) => void): (optionOverrides?: Partial) => Promise; + export default useDialogCallback; +} +declare module "shared/services/papi-core.service" { + /** Exporting empty object so people don't have to put 'type' in their import statements */ + const core: {}; + export default core; + export type { ExecutionActivationContext } from "extension-host/extension-types/extension-activation-context.model"; + export type { ExecutionToken } from "node/models/execution-token.model"; + export type { DialogTypes } from "renderer/components/dialogs/dialog-definition.model"; + export type { UseDialogCallbackOptions } from "renderer/hooks/papi-hooks/use-dialog-callback.hook"; + export type { default as IDataProvider } from "shared/models/data-provider.interface"; + export type { DataProviderUpdateInstructions, DataProviderDataType, DataProviderSubscriberOptions, } from "shared/models/data-provider.model"; + export type { WithNotifyUpdate } from "shared/models/data-provider-engine.model"; + export type { default as IDataProviderEngine } from "shared/models/data-provider-engine.model"; + export type { DialogOptions } from "shared/models/dialog-options.model"; + export type { ExtensionDataScope, MandatoryProjectDataType, } from "shared/models/project-data-provider.model"; + export type { ProjectMetadata } from "shared/models/project-metadata.model"; + export type { GetWebViewOptions, SavedWebViewDefinition, UseWebViewStateHook, WebViewContentType, WebViewDefinition, WebViewProps, } from "shared/models/web-view.model"; + export type { IWebViewProvider } from "shared/models/web-view-provider.model"; +} +declare module "shared/services/menu-data.service-model" { + import { OnDidDispose, UnsubscriberAsync, MultiColumnMenu, ReferencedItem, WebViewMenu } from 'platform-bible-utils'; + import { DataProviderDataType, DataProviderSubscriberOptions, DataProviderUpdateInstructions } from "shared/models/data-provider.model"; + import { IDataProvider } from "shared/services/papi-core.service"; + /** JSDOC DESTINATION menuDataServiceProviderName */ + export const menuDataServiceProviderName = "platform.menuDataServiceDataProvider"; + export const menuDataServiceObjectToProxy: Readonly<{ + /** + * JSDOC SOURCE menuDataServiceProviderName + * + * This name is used to register the menu data data provider on the papi. You can use this name to + * find the data provider when accessing it using the useData hook + */ + dataProviderName: "platform.menuDataServiceDataProvider"; + }>; + export type MenuDataDataTypes = { + MainMenu: DataProviderDataType; + WebViewMenu: DataProviderDataType; + }; + module 'papi-shared-types' { + interface DataProviders { + [menuDataServiceProviderName]: IMenuDataService; + } + } + /** + * JSDOC SOURCE menuDataService * * Service that allows to get and store menu data */ - menuData: IMenuDataService; - }; - export default papi; - /** This is just an alias for internet.fetch */ - export const fetch: typeof globalThis.fetch; - /** This wraps the browser's WebSocket implementation to provide - * better control over internet access. It is isomorphic with the standard WebSocket, so it should - * act as a drop-in replacement. - * - * Note that the Node WebSocket implementation is different and not wrapped here. - */ - export const WebSocket: typeof PapiRendererWebSocket; - /** This wraps the browser's XMLHttpRequest implementation to - * provide better control over internet access. It is isomorphic with the standard XMLHttpRequest, - * so it should act as a drop-in replacement. - * - * Note that Node doesn't have a native implementation, so this is only for the renderer. - */ - export const XMLHttpRequest: typeof PapiRendererXMLHttpRequest; - /** - * - * The command service allows you to exchange messages with other components in the platform. You - * can register a command that other services and extensions can send you. You can send commands to - * other services and extensions that have registered commands. - */ - export const commands: typeof commandService; - /** - * - * Service exposing various functions related to using webViews - * - * WebViews are iframes in the Platform.Bible UI into which extensions load frontend code, either - * HTML or React components. - */ - export const webViews: WebViewServiceType; - /** - * - * Prompt the user for responses with dialogs - */ - export const dialogs: DialogService; - /** - * - * Service that provides a way to send and receive network events - */ - export const network: PapiNetworkService; - /** - * - * All extensions and services should use this logger to provide a unified output of logs - */ - export const logger: import('electron-log').MainLogger & { - default: import('electron-log').MainLogger; - }; - /** - * - * Service that provides a way to call `fetch` since the original function is not available - */ - export const internet: InternetService; - /** - * - * Service that allows extensions to send and receive data to/from other extensions - */ - export const dataProviders: DataProviderService; - /** - * - * Service that registers and gets project data providers - */ - export const projectDataProviders: PapiFrontendProjectDataProviderService; - /** - * - * Provides metadata for projects known by the platform - */ - export const projectLookup: ProjectLookupServiceType; - /** - * - * React hooks that enable interacting with the `papi` in React components more easily. - */ - export const react: typeof papiReact; - /** - * - * Service that allows to get and set settings in local storage - */ - export const settings: SettingsService; - /** - * - * Service that allows to get and store menu data - */ - export const menuData: IMenuDataService; - export type Papi = typeof papi; + export type IMenuDataService = { + /** + * JSDOC SOURCE getMainMenu + * + * Get menu content for the main menu + * + * @param mainMenuType Does not have to be defined + * @returns MultiColumnMenu object of main menu content + */ + getMainMenu(mainMenuType: undefined): Promise; + /** JSDOC DESTINATION getMainMenu */ + getMainMenu(): Promise; + /** + * This data cannot be changed. Trying to use this setter this will always throw + * + * @param mainMenuType Does not have to be defined + * @param value MultiColumnMenu object to set as the main menu + * @returns Unsubscriber function + */ + setMainMenu(mainMenuType: undefined, value: never): Promise>; + /** + * Subscribe to run a callback function when the main menu data is changed + * + * @param mainMenuType Does not have to be defined + * @param callback Function to run with the updated menuContent for this selector + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber function (run to unsubscribe from listening for updates) + */ + subscribeMainMenu(mainMenuType: undefined, callback: (menuContent: MultiColumnMenu) => void, options?: DataProviderSubscriberOptions): Promise; + /** + * Get menu content for a web view + * + * @param webViewType The type of webview for which a menu should be retrieved + * @returns WebViewMenu object of web view menu content + */ + getWebViewMenu(webViewType: ReferencedItem): Promise; + /** + * This data cannot be changed. Trying to use this setter this will always throw + * + * @param webViewType The type of webview for which a menu should be set + * @param value Menu of specified webViewType + * @returns Unsubscriber function + */ + setWebViewMenu(webViewType: ReferencedItem, value: never): Promise>; + /** + * Subscribe to run a callback function when the web view menu data is changed + * + * @param webViewType The type of webview for which a menu should be subscribed + * @param callback Function to run with the updated menuContent for this selector + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber function (run to unsubscribe from listening for updates) + */ + subscribeWebViewMenu(webViewType: ReferencedItem, callback: (menuContent: WebViewMenu) => void, options?: DataProviderSubscriberOptions): Promise; + } & OnDidDispose & typeof menuDataServiceObjectToProxy & IDataProvider; +} +declare module "shared/services/menu-data.service" { + import { IMenuDataService } from "shared/services/menu-data.service-model"; + const menuDataService: IMenuDataService; + export default menuDataService; +} +declare module "extension-host/services/papi-backend.service" { + /** + * Unified module for accessing API features in the extension host. + * + * WARNING: DO NOT IMPORT papi IN ANY FILE THAT papi IMPORTS AND EXPOSES. + */ + import * as commandService from "shared/services/command.service"; + import { PapiNetworkService } from "shared/services/network.service"; + 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 { 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"; + const papi: { + /** JSDOC DESTINATION DataProviderEngine */ + DataProviderEngine: typeof PapiDataProviderEngine; + /** This is just an alias for internet.fetch */ + fetch: typeof globalThis.fetch; + /** JSDOC DESTINATION commandService */ + commands: typeof commandService; + /** JSDOC DESTINATION papiWebViewService */ + webViews: WebViewServiceType; + /** JSDOC DESTINATION papiWebViewProviderService */ + webViewProviders: PapiWebViewProviderService; + /** JSDOC DESTINATION dialogService */ + dialogs: DialogService; + /** JSDOC DESTINATION papiNetworkService */ + network: PapiNetworkService; + /** JSDOC DESTINATION logger */ + logger: import("electron-log").MainLogger & { + default: import("electron-log").MainLogger; + }; + /** JSDOC DESTINATION internetService */ + internet: InternetService; + /** JSDOC DESTINATION dataProviderService */ + dataProviders: DataProviderService; + /** JSDOC DESTINATION papiBackendProjectDataProviderService */ + projectDataProviders: PapiBackendProjectDataProviderService; + /** JSDOC DESTINATION projectLookupService */ + projectLookup: ProjectLookupServiceType; + /** JSDOC DESTINATION extensionStorageService */ + storage: ExtensionStorageService; + /** JSDOC DESTINATION menuDataService */ + menuData: IMenuDataService; + }; + export default papi; + /** JSDOC DESTINATION DataProviderEngine */ + export const DataProviderEngine: typeof PapiDataProviderEngine; + /** This is just an alias for internet.fetch */ + export const fetch: typeof globalThis.fetch; + /** JSDOC DESTINATION commandService */ + export const commands: typeof commandService; + /** JSDOC DESTINATION papiWebViewService */ + export const webViews: WebViewServiceType; + /** JSDOC DESTINATION papiWebViewProviderService */ + export const webViewProviders: PapiWebViewProviderService; + /** JSDOC DESTINATION dialogService */ + export const dialogs: DialogService; + /** JSDOC DESTINATION papiNetworkService */ + export const network: PapiNetworkService; + /** JSDOC DESTINATION logger */ + export const logger: import("electron-log").MainLogger & { + default: import("electron-log").MainLogger; + }; + /** JSDOC DESTINATION internetService */ + export const internet: InternetService; + /** JSDOC DESTINATION dataProviderService */ + export const dataProviders: DataProviderService; + /** JSDOC DESTINATION papiBackendProjectDataProviderService */ + export const projectDataProviders: PapiBackendProjectDataProviderService; + /** JSDOC DESTINATION projectLookupService */ + export const projectLookup: ProjectLookupServiceType; + /** JSDOC DESTINATION extensionStorageService */ + export const storage: ExtensionStorageService; + /** JSDOC DESTINATION menuDataService */ + export const menuData: IMenuDataService; +} +declare module "extension-host/extension-types/extension.interface" { + import { UnsubscriberAsync } from 'platform-bible-utils'; + import { ExecutionActivationContext } from "extension-host/extension-types/extension-activation-context.model"; + /** Interface for all extensions to implement */ + export interface IExtension { + /** + * Sets up this extension! Runs when paranext wants this extension to activate. For example, + * activate() should register commands for this extension + * + * @param context Data and utilities that are specific to this particular extension + */ + activate: (context: ExecutionActivationContext) => Promise; + /** + * Deactivate anything in this extension that is not covered by the registrations in the context + * object given to activate(). + * + * @returns Promise that resolves to true if successfully deactivated + */ + deactivate?: UnsubscriberAsync; + } +} +declare module "extension-host/extension-types/extension-manifest.model" { + /** Information about an extension provided by the extension developer. */ + export type ExtensionManifest = { + /** Name of the extension */ + name: string; + /** + * Extension version - expected to be [semver](https://semver.org/) like `"0.1.3"`. + * + * Note: semver may become a hard requirement in the future, so we recommend using it now. + */ + version: string; + /** + * Path to the JavaScript file to run in the extension host. Relative to the extension's root + * folder. + * + * Must be specified. Can be an empty string if the extension does not have any JavaScript to run. + */ + main: string; + /** + * Path to the TypeScript type declaration file that describes this extension and its interactions + * on the PAPI. Relative to the extension's root folder. + * + * If not provided, Platform.Bible will look in the following locations: + * + * 1. `.d.ts` + * 2. `.d.ts` + * 3. `index.d.ts` + * + * See [Extension Anatomy - Type Declaration + * Files](https://github.com/paranext/paranext-extension-template/wiki/Extension-Anatomy#type-declaration-files-dts) + * for more information about extension type declaration files. + */ + types?: string; + /** + * List of events that occur that should cause this extension to be activated. Not yet + * implemented. + */ + activationEvents: string[]; + }; +} +declare module "shared/services/settings.service-model" { + import { SettingNames, SettingTypes } from 'papi-shared-types'; + import { OnDidDispose, PlatformEventEmitter, Unsubscriber } from 'platform-bible-utils'; + import { DataProviderUpdateInstructions, IDataProvider } from "shared/services/papi-core.service"; + /** JSDOC DESTINATION dataProviderName */ + export const settingsServiceDataProviderName = "platform.settingsServiceDataProvider"; + export const settingsServiceObjectToProxy: Readonly<{ + /** + * JSDOC SOURCE dataProviderName + * + * Name used to register the data provider + * + * You can use this name + */ + dataProviderName: "platform.settingsServiceDataProvider"; + }>; + /** + * SettingDataTypes handles getting and setting Platform.Bible core application and extension + * settings. + * + * Note: the unnamed (`''`) data type is not actually part of `SettingDataTypes` because the methods + * would not be able to create a generic type extending from `SettingNames` in order to return the + * specific setting type being requested. As such, `get`, `set`, `reset` and `subscribe` are all + * specified on {@link ISettingsService} instead. Unfortunately, as a result, using Intellisense with + * `useData` will not show the unnamed data type (`''`) as an option, but you can use `useSetting` + * instead. However, do note that the unnamed data type (`''`) is fully functional. + */ + export type SettingDataTypes = {}; + module 'papi-shared-types' { + interface DataProviders { + [settingsServiceDataProviderName]: ISettingsService; + } + } + /** Event to set or update a setting */ + export type UpdateSettingEvent = { + type: 'update-setting'; + setting: SettingTypes[SettingName]; + }; + /** Event to remove a setting */ + export type ResetSettingEvent = { + type: 'reset-setting'; + }; + /** All supported setting events */ + export type SettingEvent = UpdateSettingEvent | ResetSettingEvent; + /** All message subscriptions - emitters that emit an event each time a setting is updated */ + export const onDidUpdateSettingEmitters: Map>>; + /** JSDOC SOURCE settingsService */ + export type ISettingsService = { + /** + * Retrieves the value of the specified setting + * + * @param key The string id of the setting for which the value is being retrieved + * @param defaultSetting The default value used for the setting if no value is available for the + * key + * @returns The value of the specified setting, parsed to an object. Returns default setting if + * setting does not exist + */ + get(key: SettingName): Promise; + /** + * Sets the value of the specified setting + * + * @param key The string id of the setting for which the value is being retrieved + * @param newSetting The value that is to be stored. Setting the new value to `undefined` is the + * equivalent of deleting the setting + */ + set(key: SettingName, newSetting: SettingTypes[SettingName]): Promise>; + /** + * Removes the setting from memory + * + * @param key The string id of the setting for which the value is being removed + * @returns `true` if successfully reset the project setting. `false` otherwise + */ + reset(key: SettingName): Promise; + /** + * Subscribes to updates of the specified setting. Whenever the value of the setting changes, the + * callback function is executed. + * + * @param key The string id of the setting for which the value is being subscribed to + * @param callback The function that will be called whenever the specified setting is updated + * @returns Unsubscriber that should be called whenever the subscription should be deleted + */ + subscribe(key: SettingName, callback: (newSetting: SettingEvent) => void): Promise; + } & OnDidDispose & IDataProvider & typeof settingsServiceObjectToProxy; +} +declare module "shared/services/settings.service" { + import { ISettingsService } from "shared/services/settings.service-model"; + const settingsService: ISettingsService; + export default settingsService; +} +declare module "renderer/hooks/hook-generators/create-use-network-object-hook.util" { + import { NetworkObject } from "shared/models/network-object.model"; + /** + * This function takes in a getNetworkObject function and creates a hook with that function in it + * which will return a network object + * + * @param getNetworkObject A function that takes in an id string and returns a network object + * @param mapParametersToNetworkObjectSource Function that takes the parameters passed into the hook + * and returns the `networkObjectSource` associated with those parameters. Defaults to taking the + * first parameter passed into the hook and using that as the `networkObjectSource`. + * + * - Note: `networkObjectSource` is string name of the network object to get OR `networkObject` + * (result of this hook, if you want this hook to just return the network object again) + * + * @returns A function that takes in a networkObjectSource and returns a NetworkObject + */ + function createUseNetworkObjectHook(getNetworkObject: (...args: THookParams) => Promise | undefined>, mapParametersToNetworkObjectSource?: (...args: THookParams) => string | NetworkObject | undefined): (...args: THookParams) => NetworkObject | undefined; + export default createUseNetworkObjectHook; +} +declare module "renderer/hooks/papi-hooks/use-data-provider.hook" { + import { DataProviders } from 'papi-shared-types'; + /** + * Gets a data provider with specified provider name + * + * @type `T` - The type of data provider to return. Use `IDataProvider`, + * specifying your own types, or provide a custom data provider type + * @param dataProviderSource String name of the data provider to get OR dataProvider (result of + * useDataProvider, if you want this hook to just return the data provider again) + * @returns Undefined if the data provider has not been retrieved, data provider if it has been + * retrieved and is not disposed, and undefined again if the data provider is disposed + */ + const useDataProvider: (dataProviderSource: DataProviderName | DataProviders[DataProviderName] | undefined) => DataProviders[DataProviderName] | undefined; + export default useDataProvider; +} +declare module "renderer/hooks/hook-generators/create-use-data-hook.util" { + import { DataProviderSubscriberOptions, DataProviderUpdateInstructions } from "shared/models/data-provider.model"; + import IDataProvider from "shared/models/data-provider.interface"; + import ExtractDataProviderDataTypes from "shared/models/extract-data-provider-data-types.model"; + /** + * The final function called as part of the `useData` hook that is the actual React hook + * + * This is the `.Greeting(...)` part of `useData('helloSomeone.people').Greeting(...)` + */ + type UseDataFunctionWithProviderType, TDataType extends keyof ExtractDataProviderDataTypes> = (selector: ExtractDataProviderDataTypes[TDataType]['selector'], defaultValue: ExtractDataProviderDataTypes[TDataType]['getData'], subscriberOptions?: DataProviderSubscriberOptions) => [ + ExtractDataProviderDataTypes[TDataType]['getData'], + (((newData: ExtractDataProviderDataTypes[TDataType]['setData']) => Promise>>) | undefined), + boolean + ]; + /** + * A proxy that serves the actual hooks for a single data provider + * + * This is the `useData('helloSomeone.people')` part of + * `useData('helloSomeone.people').Greeting(...)` + */ + type UseDataProxy> = { + [TDataType in keyof ExtractDataProviderDataTypes]: UseDataFunctionWithProviderType; + }; + /** + * React hook to use data provider data with various data types + * + * @example `useData('helloSomeone.people').Greeting('Bill', 'Greeting loading')` + * + * @type `TDataProvider` - The type of data provider to get. Use + * `IDataProvider`, specifying your own types, or provide a custom data + * provider type + */ + type UseDataHookGeneric = { + >(...args: TUseDataProviderParams): UseDataProxy; + }; + /** + * Create a `useData(...).DataType(selector, defaultValue, options)` hook for a specific subset of + * data providers as supported by `useDataProviderHook` + * + * @param useDataProviderHook Hook that gets a data provider from a specific subset of data + * providers + * @returns `useData` hook for getting data from a data provider + */ + function createUseDataHook(useDataProviderHook: (...args: TUseDataProviderParams) => IDataProvider | undefined): UseDataHookGeneric; + export default createUseDataHook; +} +declare module "renderer/hooks/papi-hooks/use-data.hook" { + import { DataProviderSubscriberOptions, DataProviderUpdateInstructions } from "shared/models/data-provider.model"; + import { DataProviderNames, DataProviderTypes, DataProviders } from 'papi-shared-types'; + /** + * React hook to use data from a data provider + * + * @example `useData('helloSomeone.people').Greeting('Bill', 'Greeting loading')` + */ + type UseDataHook = { + (dataProviderSource: DataProviderName | DataProviders[DataProviderName] | undefined): { + [TDataType in keyof DataProviderTypes[DataProviderName]]: (selector: DataProviderTypes[DataProviderName][TDataType]['selector'], defaultValue: DataProviderTypes[DataProviderName][TDataType]['getData'], subscriberOptions?: DataProviderSubscriberOptions) => [ + DataProviderTypes[DataProviderName][TDataType]['getData'], + (((newData: DataProviderTypes[DataProviderName][TDataType]['setData']) => Promise>) | undefined), + boolean + ]; + }; + }; + /** + * ```typescript + * useData( + * dataProviderSource: DataProviderName | DataProviders[DataProviderName] | undefined, + * ).DataType( + * selector: DataProviderTypes[DataProviderName][DataType]['selector'], + * defaultValue: DataProviderTypes[DataProviderName][DataType]['getData'], + * subscriberOptions?: DataProviderSubscriberOptions, + * ) => [ + * DataProviderTypes[DataProviderName][DataType]['getData'], + * ( + * | (( + * newData: DataProviderTypes[DataProviderName][DataType]['setData'], + * ) => Promise>) + * | undefined + * ), + * boolean, + * ] + * ``` + * + * React hook to use data from a data provider. Subscribes to run a callback on a data provider's + * data with specified selector on the specified data type that data provider serves. + * + * Usage: Specify the data provider and the data type on the data provider with + * `useData('').` and use like any other React hook. + * + * _@example_ Subscribing to Verse data at JHN 11:35 on the `'quickVerse.quickVerse'` data provider: + * + * ```typescript + * const [verseText, setVerseText, verseTextIsLoading] = useData('quickVerse.quickVerse').Verse( + * 'JHN 11:35', + * 'Verse text goes here', + * ); + * ``` + * + * _@param_ `dataProviderSource` string name of data provider to get OR dataProvider (result of + * useDataProvider if you want to consolidate and only get the data provider once) + * + * _@param_ `selector` tells the provider what data this listener is listening for + * + * 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 data + * + * _@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_ `[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 + * - `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 + * 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 + * provider + */ + const useData: UseDataHook; + export default useData; +} +declare module "renderer/hooks/papi-hooks/use-setting.hook" { + import { SettingTypes } from 'papi-shared-types'; + import { DataProviderSubscriberOptions, DataProviderUpdateInstructions } from "shared/models/data-provider.model"; + import { SettingDataTypes } from "shared/services/settings.service-model"; + /** + * Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes + * and gets updated when the setting is changed by others. + * + * @param key The string id that is used to store the setting in local storage + * + * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be + * updated every render + * @param defaultState The default state of the setting. If the setting already has a value set to + * it in local storage, this parameter will be ignored. + * + * Note: this parameter is internally assigned to a `ref`, so changing it will not cause any hooks + * to re-run with its new value. Running `resetSetting()` will always update the setting value + * returned to the latest `defaultState`, and changing the `key` will use the latest + * `defaultState`. However, if `defaultState` is changed while a setting is `defaultState` + * (meaning it is reset and has no value), the returned setting value will not be updated to the + * new `defaultState`. + * @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 state of the setting, either `defaultState` or the stored state on the + * papi, if any + * - `setSetting`: Function that updates the setting to a new value + * - `resetSetting`: Function that removes the setting and resets the value to `defaultState` + * + * @throws When subscription callback function is called with an update that has an unexpected + * message type + */ + const useSetting: (key: SettingName, defaultState: SettingTypes[SettingName], subscriberOptions?: DataProviderSubscriberOptions) => [setting: SettingTypes[SettingName], setSetting: (newData: SettingTypes[SettingName]) => Promise>, resetSetting: () => void]; + export default useSetting; +} +declare module "renderer/hooks/papi-hooks/use-project-data-provider.hook" { + import { ProjectDataProviders } from 'papi-shared-types'; + /** + * 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 + * 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 + */ + const useProjectDataProvider: (projectType: ProjectType, projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined) => ProjectDataProviders[ProjectType] | undefined; + export default useProjectDataProvider; +} +declare module "renderer/hooks/papi-hooks/use-project-data.hook" { + import { DataProviderSubscriberOptions, DataProviderUpdateInstructions } from "shared/models/data-provider.model"; + import { ProjectDataProviders, ProjectDataTypes, ProjectTypes } from 'papi-shared-types'; + /** + * React hook to use data from a project data provider + * + * @example `useProjectData('ParatextStandard', 'project id').VerseUSFM(...);` + */ + type UseProjectDataHook = { + (projectType: ProjectType, projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined): { + [TDataType in keyof ProjectDataTypes[ProjectType]]: (selector: ProjectDataTypes[ProjectType][TDataType]['selector'], defaultValue: ProjectDataTypes[ProjectType][TDataType]['getData'], subscriberOptions?: DataProviderSubscriberOptions) => [ + ProjectDataTypes[ProjectType][TDataType]['getData'], + (((newData: ProjectDataTypes[ProjectType][TDataType]['setData']) => Promise>) | undefined), + boolean + ]; + }; + }; + /** + * ```typescript + * useProjectData( + * projectType: ProjectType, + * projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined, + * ).DataType( + * selector: ProjectDataTypes[ProjectType][DataType]['selector'], + * defaultValue: ProjectDataTypes[ProjectType][DataType]['getData'], + * subscriberOptions?: DataProviderSubscriberOptions, + * ) => [ + * ProjectDataTypes[ProjectType][DataType]['getData'], + * ( + * | (( + * newData: ProjectDataTypes[ProjectType][DataType]['setData'], + * ) => Promise>) + * | undefined + * ), + * boolean, + * ] + * ``` + * + * 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 + * with `useProjectData('', '').` and use like any other React + * hook. + * + * _@example_ Subscribing to Verse USFM info at JHN 11:35 on a `ParatextStandard` project with + * projectId `32664dc3288a28df2e2bb75ded887fc8f17a15fb`: + * + * ```typescript + * const [verse, setVerse, verseIsLoading] = useProjectData( + * 'ParatextStandard', + * '32664dc3288a28df2e2bb75ded887fc8f17a15fb', + * ).VerseUSFM( + * useMemo(() => new VerseRef('JHN', '11', '35', ScrVers.English), []), + * 'Loading verse ', + * ); + * ``` + * + * _@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 + * 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) + * + * _@param_ `selector` tells the provider what data this listener is listening for + * + * 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 data + * + * _@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 + * until `projectDataProviderSource` or `selector` changes. + * + * _@returns_ `[data, setData, isLoading]` + */ + const useProjectData: UseProjectDataHook; + export default useProjectData; +} +declare module "renderer/hooks/papi-hooks/use-data-provider-multi.hook" { + import { DataProviderNames, DataProviders } from 'papi-shared-types'; + /** + * Gets an array of data providers based on an array of input sources + * + * @type `T` - The types of data providers to return. Use `IDataProvider`, + * specifying your own types, or provide a custom data provider type for each item in the array. + * Note that if you provide more than one data type, each item in the returned array will be + * considered to be any of those types. For example, if you call `useDataProviderMulti`, all items in the returned array will be considered to be of type `Type1 | Type2 | + * undefined`. Although you can determine the actual type based on the array index, TypeScript + * will not know, so you will need to type assert the array items for later type checking to + * work. + * @param dataProviderSources Array containing string names of the data providers to get OR data + * providers themselves (i.e., the results of useDataProvider/useDataProviderMulti) if you want + * this hook to return the data providers again. It is fine to have a mix of strings and data + * providers in the array. + * + * WARNING: THE ARRAY MUST BE STABLE - const or wrapped in useState, useMemo, etc. It must not be + * updated every render. + * @returns An array of data providers that correspond by index to the values in + * `dataProviderSources`. Each item in the array will be (a) undefined if the data provider has + * not been retrieved or has been disposed, or (b) a data provider if it has been retrieved and is + * not disposed. + */ + function useDataProviderMulti(dataProviderSources: (EachDataProviderName[number] | DataProviders[EachDataProviderName[number]] | undefined)[]): (DataProviders[EachDataProviderName[number]] | undefined)[]; + export default useDataProviderMulti; +} +declare module "renderer/hooks/papi-hooks/index" { + export { default as useDataProvider } from "renderer/hooks/papi-hooks/use-data-provider.hook"; + 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 useDialogCallback } from "renderer/hooks/papi-hooks/use-dialog-callback.hook"; + export { default as useDataProviderMulti } from "renderer/hooks/papi-hooks/use-data-provider-multi.hook"; +} +declare module "renderer/services/papi-frontend-react.service" { + export * from "renderer/hooks/papi-hooks/index"; +} +declare module "renderer/services/renderer-xml-http-request.service" { + /** + * JSDOC SOURCE PapiRendererXMLHttpRequest This wraps the browser's XMLHttpRequest implementation to + * provide better control over internet access. It is isomorphic with the standard XMLHttpRequest, + * so it should act as a drop-in replacement. + * + * Note that Node doesn't have a native implementation, so this is only for the renderer. + */ + export default class PapiRendererXMLHttpRequest implements XMLHttpRequest { + readonly DONE: 4; + readonly HEADERS_RECEIVED: 2; + readonly LOADING: 3; + readonly OPENED: 1; + readonly UNSENT: 0; + abort: () => void; + addEventListener: (type: K, listener: (this: XMLHttpRequest, ev: XMLHttpRequestEventMap[K]) => any, options?: boolean | AddEventListenerOptions) => void; + dispatchEvent: (event: Event) => boolean; + getAllResponseHeaders: () => string; + getResponseHeader: (name: string) => string | null; + open: (method: string, url: string, async?: boolean, username?: string | null, password?: string | null) => void; + onabort: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + onerror: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + onload: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + onloadend: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + onloadstart: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + onprogress: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + onreadystatechange: ((this: XMLHttpRequest, ev: Event) => any) | null; + ontimeout: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + overrideMimeType: (mime: string) => void; + readyState: number; + removeEventListener: (type: K, listener: (this: XMLHttpRequest, ev: XMLHttpRequestEventMap[K]) => any, options?: boolean | EventListenerOptions) => void; + response: any; + responseText: string; + responseType: XMLHttpRequestResponseType; + responseURL: string; + responseXML: Document | null; + send: (body?: Document | XMLHttpRequestBodyInit | null) => void; + setRequestHeader: (name: string, value: string) => void; + status: number; + statusText: string; + timeout: number; + upload: XMLHttpRequestUpload; + withCredentials: boolean; + constructor(); + } +} +declare module "renderer/services/papi-frontend.service" { + /** + * Unified module for accessing API features in the renderer. + * + * WARNING: DO NOT IMPORT papi IN ANY FILE THAT papi IMPORTS AND EXPOSES. + */ + import * as commandService from "shared/services/command.service"; + import { PapiNetworkService } from "shared/services/network.service"; + import { WebViewServiceType } from "shared/services/web-view.service-model"; + import { InternetService } from "shared/services/internet.service"; + import { DataProviderService } from "shared/services/data-provider.service"; + import { ProjectLookupServiceType } from "shared/services/project-lookup.service-model"; + import { PapiFrontendProjectDataProviderService } from "shared/services/project-data-provider.service"; + import { DialogService } from "shared/services/dialog.service-model"; + import * as papiReact from "renderer/services/papi-frontend-react.service"; + import PapiRendererWebSocket from "renderer/services/renderer-web-socket.service"; + import { IMenuDataService } from "shared/services/menu-data.service-model"; + import PapiRendererXMLHttpRequest from "renderer/services/renderer-xml-http-request.service"; + const papi: { + /** This is just an alias for internet.fetch */ + fetch: typeof globalThis.fetch; + /** JSDOC DESTINATION PapiRendererWebSocket */ + WebSocket: typeof PapiRendererWebSocket; + /** JSDOC DESTINATION PapiRendererXMLHttpRequest */ + XMLHttpRequest: typeof PapiRendererXMLHttpRequest; + /** JSDOC DESTINATION commandService */ + commands: typeof commandService; + /** JSDOC DESTINATION papiWebViewService */ + webViews: WebViewServiceType; + /** JSDOC DESTINATION dialogService */ + dialogs: DialogService; + /** JSDOC DESTINATION papiNetworkService */ + network: PapiNetworkService; + /** JSDOC DESTINATION logger */ + logger: import("electron-log").MainLogger & { + default: import("electron-log").MainLogger; + }; + /** JSDOC DESTINATION internetService */ + internet: InternetService; + /** JSDOC DESTINATION dataProviderService */ + dataProviders: DataProviderService; + /** JSDOC DESTINATION papiFrontendProjectDataProviderService */ + projectDataProviders: PapiFrontendProjectDataProviderService; + /** JSDOC DESTINATION projectLookupService */ + projectLookup: ProjectLookupServiceType; + /** + * JSDOC SOURCE papiReact + * + * React hooks that enable interacting with the `papi` in React components more easily. + */ + react: typeof papiReact; + /** JSDOC DESTINATION settingsService */ + settings: SettingsService; + /** JSDOC DESTINATION menuDataService */ + menuData: IMenuDataService; + }; + export default papi; + /** This is just an alias for internet.fetch */ + export const fetch: typeof globalThis.fetch; + /** JSDOC DESTINATION PapiRendererWebSocket */ + export const WebSocket: typeof PapiRendererWebSocket; + /** JSDOC DESTINATION PapiRendererXMLHttpRequest */ + export const XMLHttpRequest: typeof PapiRendererXMLHttpRequest; + /** JSDOC DESTINATION commandService */ + export const commands: typeof commandService; + /** JSDOC DESTINATION papiWebViewService */ + export const webViews: WebViewServiceType; + /** JSDOC DESTINATION dialogService */ + export const dialogs: DialogService; + /** JSDOC DESTINATION papiNetworkService */ + export const network: PapiNetworkService; + /** JSDOC DESTINATION logger */ + export const logger: import("electron-log").MainLogger & { + default: import("electron-log").MainLogger; + }; + /** JSDOC DESTINATION internetService */ + export const internet: InternetService; + /** JSDOC DESTINATION dataProviderService */ + export const dataProviders: DataProviderService; + /** JSDOC DESTINATION papiBackendProjectDataProviderService */ + export const projectDataProviders: PapiFrontendProjectDataProviderService; + /** JSDOC DESTINATION projectLookupService */ + export const projectLookup: ProjectLookupServiceType; + /** JSDOC DESTINATION papiReact */ + export const react: typeof papiReact; + /** JSDOC DESTINATION settingsService */ + export const settings: SettingsService; + /** JSDOC DESTINATION menuDataService */ + export const menuData: IMenuDataService; + export type Papi = typeof papi; } diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index 18a501675e..81e2ed101f 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -2,7 +2,6 @@ import IDataProviderEngine from '@shared/models/data-provider-engine.model'; import { DataProviderUpdateInstructions } from '@shared/models/data-provider.model'; import dataProviderService, { DataProviderEngine } from '@shared/services/data-provider.service'; import { - createSyncProxyForAsyncObject, ISettingsService, ResetSettingEvent, SettingDataTypes, @@ -11,25 +10,23 @@ import { onDidUpdateSettingEmitters, settingsServiceDataProviderName, settingsServiceObjectToProxy, - SettingsValues, } from '@shared/services/settings.service-model'; -import * as nodeFS from '@node/services/node-file-system.service'; import coreSettingsInfo, { AllSettingsInfo } from '@main/data/core-settings-info.data'; import { SettingNames, SettingTypes } from 'papi-shared-types'; import { + createSyncProxyForAsyncObject, PlatformEventEmitter, - UnsubscriberAsync, + // UnsubscriberAsync, deserialize, serialize, + Unsubscriber, } from 'platform-bible-utils'; -import { joinUriPaths } from '@node/utils/util'; -// TODO: 3 Fix function declarations to match service-model // TODO: 4 Fix implementation of all functions // TODO: Where do settings live (JSON obj/file)? How is dp going to access it? class SettingDataProviderEngine - extends DataProviderEngine> - implements IDataProviderEngine> + extends DataProviderEngine + implements IDataProviderEngine { private settingsInfo; // eslint-disable-next-line @typescript-eslint/no-useless-constructor @@ -39,9 +36,8 @@ class SettingDataProviderEngine } // eslint-disable-next-line class-methods-use-this - async getSetting( + async get( key: SettingName, - // defaultSetting: SettingTypes[SettingName], ): Promise { const settingString = localStorage.getItem(key); // Null is used by the external API @@ -55,10 +51,10 @@ class SettingDataProviderEngine } // eslint-disable-next-line class-methods-use-this - async setSetting( + async set( key: SettingName, newSetting: SettingTypes[SettingName], - ): Promise>> { + ): Promise> { localStorage.setItem(key, serialize(newSetting)); // Assert type of the particular SettingName of the emitter. // eslint-disable-next-line no-type-assertion/no-type-assertion @@ -72,9 +68,7 @@ class SettingDataProviderEngine } // eslint-disable-next-line class-methods-use-this - async resetSetting( - key: SettingName, - ): Promise>> { + async reset(key: SettingName): Promise { localStorage.removeItem(key); // Assert type of the particular SettingName of the emitter. // eslint-disable-next-line no-type-assertion/no-type-assertion @@ -85,10 +79,10 @@ class SettingDataProviderEngine } // eslint-disable-next-line class-methods-use-this - async subscribeSetting( + async subscribe( key: SettingName, callback: (newSetting: SettingEvent) => void, - ): Promise { + ): Promise { // Assert type of the particular SettingName of the emitter. // eslint-disable-next-line no-type-assertion/no-type-assertion let emitter = onDidUpdateSettingEmitters.get(key) as @@ -105,16 +99,6 @@ class SettingDataProviderEngine } return emitter.subscribe(callback); } - - // eslint-disable-next-line class-methods-use-this - async #loadSettings(): Promise { - const settingString = await nodeFS.readFileText( - joinUriPaths('shared', 'data', 'settings-values.ts'), - ); - if (!settingString) throw new Error('Error reading settings'); - const settingsObj: SettingsValues = deserialize(settingString); - return settingsObj; - } } let initializationPromise: Promise; @@ -143,7 +127,7 @@ export async function initialize(): Promise { /** This is an internal-only export for testing purposes, and should not be used in development */ export const testingSettingService = { implementSettingDataProviderEngine: () => { - return new SettingDataProviderEngine(); + return new SettingDataProviderEngine(coreSettingsInfo); }, }; diff --git a/src/renderer/hooks/papi-hooks/use-setting.hook.ts b/src/renderer/hooks/papi-hooks/use-setting.hook.ts index 99401839ac..3ad0b8bb87 100644 --- a/src/renderer/hooks/papi-hooks/use-setting.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-setting.hook.ts @@ -1,6 +1,12 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useRef } from 'react'; import settingsService from '@shared/services/settings.service'; import { SettingNames, SettingTypes } from 'papi-shared-types'; +import useData from '@renderer/hooks/papi-hooks/use-data.hook'; +import { + DataProviderSubscriberOptions, + DataProviderUpdateInstructions, +} from '@shared/models/data-provider.model'; +import { SettingDataTypes } from '@shared/services/settings.service-model'; /** * Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes @@ -19,6 +25,12 @@ import { SettingNames, SettingTypes } from 'papi-shared-types'; * `defaultState`. However, if `defaultState` is changed while a setting is `defaultState` * (meaning it is reset and has no value), the returned setting value will not be updated to the * new `defaultState`. + * @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 state of the setting, either `defaultState` or the stored state on the @@ -32,48 +44,33 @@ import { SettingNames, SettingTypes } from 'papi-shared-types'; const useSetting = ( key: SettingName, defaultState: SettingTypes[SettingName], + subscriberOptions?: DataProviderSubscriberOptions, ): [ setting: SettingTypes[SettingName], - setSetting: (newSetting: SettingTypes[SettingName]) => void, + setSetting: ( + newData: SettingTypes[SettingName], + ) => Promise>, resetSetting: () => void, ] => { // Use defaultState as a ref so it doesn't update dependency arrays const defaultStateRef = useRef(defaultState); defaultStateRef.current = defaultState; - const [setting, setSettingInternal] = useState(settingsService.get(key, defaultStateRef.current)); - - useEffect(() => { - // Get the setting for the new key when the key changes - setSettingInternal(settingsService.get(key, defaultStateRef.current)); - - // Subscribe to changes to the setting - const unsubscriber = settingsService.subscribe(key, (newSetting) => { - if (newSetting.type === 'update-setting') { - setSettingInternal(newSetting.setting); - } else if (newSetting.type === 'reset-setting') { - setSettingInternal(defaultStateRef.current); - } else { - throw new Error('Unexpected message type used for updating setting'); - } - }); - - return () => { - unsubscriber(); - }; - }, [key]); - - const setSetting = useCallback( - (newSetting: SettingTypes[SettingName]) => { - settingsService.set(key, newSetting); - setSettingInternal(newSetting); - }, - [key], - ); + // eslint-disable-next-line no-type-assertion/no-type-assertion + const [setting, setSetting] = useData(settingsService)['']( + key, + defaultState, + subscriberOptions, + ) as [ + setting: SettingTypes[SettingName], + setSetting: ( + newData: SettingTypes[SettingName], + ) => Promise>, + boolean, + ]; const resetSetting = useCallback(() => { settingsService.reset(key); - setSettingInternal(defaultStateRef.current); }, [key]); return [setting, setSetting, resetSetting]; diff --git a/src/shared/services/settings.service-model.ts b/src/shared/services/settings.service-model.ts index 8c08b2d936..fa7fc1e895 100644 --- a/src/shared/services/settings.service-model.ts +++ b/src/shared/services/settings.service-model.ts @@ -1,11 +1,11 @@ import { SettingNames, SettingTypes } from 'papi-shared-types'; -import { OnDidDispose, PlatformEventEmitter, UnsubscriberAsync } from 'platform-bible-utils'; import { - DataProviderDataType, - DataProviderSubscriberOptions, - DataProviderUpdateInstructions, - IDataProvider, -} from './papi-core.service'; + OnDidDispose, + PlatformEventEmitter, + Unsubscriber, + // UnsubscriberAsync, +} from 'platform-bible-utils'; +import { DataProviderUpdateInstructions, IDataProvider } from './papi-core.service'; /** JSDOC DESTINATION dataProviderName */ export const settingsServiceDataProviderName = 'platform.settingsServiceDataProvider'; @@ -19,8 +19,17 @@ export const settingsServiceObjectToProxy = Object.freeze({ */ dataProviderName: settingsServiceDataProviderName, }); - -// TODO: 1 Fix types- should they be like string/T or SettingName extends SettingNames +/** + * SettingDataTypes handles getting and setting Platform.Bible core application and extension + * settings. + * + * Note: the unnamed (`''`) data type is not actually part of `SettingDataTypes` because the methods + * would not be able to create a generic type extending from `SettingNames` in order to return the + * specific setting type being requested. As such, `get`, `set`, `reset` and `subscribe` are all + * specified on {@link ISettingsService} instead. Unfortunately, as a result, using Intellisense with + * `useData` will not show the unnamed data type (`''`) as an option, but you can use `useSetting` + * instead. However, do note that the unnamed data type (`''`) is fully functional. + */ export type SettingDataTypes = { // '': DataProviderDataType; }; @@ -53,7 +62,6 @@ export const onDidUpdateSettingEmitters = new Map< PlatformEventEmitter> >(); -// TODO: 2 Fix function declarations to match data provider data types /** JSDOC SOURCE settingsService */ export type ISettingsService = { /** @@ -65,10 +73,7 @@ export type ISettingsService = { * @returns The value of the specified setting, parsed to an object. Returns default setting if * setting does not exist */ - get( - key: SettingName, - defaultSetting: SettingTypes[SettingName], - ): Promise; + get(key: SettingName): Promise; /** * Sets the value of the specified setting @@ -101,37 +106,7 @@ export type ISettingsService = { subscribe( key: SettingName, callback: (newSetting: SettingEvent) => void, - options?: DataProviderSubscriberOptions, - ): Promise; + ): Promise; } & OnDidDispose & IDataProvider & typeof settingsServiceObjectToProxy; - -// eslint-disable-next-line no-type-assertion/no-type-assertion -const blah = {} as ISettingsService; -const thing = await blah.get('platform.verseRef'); - -// TODO: delete this utility function once menuDataService is pushed- don't have access to it now -export function createSyncProxyForAsyncObject( - getObject: (args?: unknown[]) => Promise, - objectToProxy: Partial = {}, -): T { - // objectToProxy will have only the synchronously accessed properties of T on it, and this proxy - // makes the async methods that do not exist yet available synchronously so we have all of T - // eslint-disable-next-line no-type-assertion/no-type-assertion - return new Proxy(objectToProxy as T, { - get(target, prop) { - // We don't have any type information for T, so we assume methodName exists on it and will let JavaScript throw if it doesn't exist - // @ts-expect-error 7053 - if (prop in target) return target[prop]; - return async (...args: unknown[]) => { - // 7053: We don't have any type information for T, so we assume methodName exists on it and will let JavaScript throw if it doesn't exist - // 2556: The args here are the parameters for the method specified - // @ts-expect-error 7053 2556 - return (await getObject())[prop](...args); - }; - }, - }); -} - -export type SettingsValues = { [settingId: string]: unknown }; diff --git a/src/shared/services/settings.service.ts b/src/shared/services/settings.service.ts index 5f17bb9555..c4d455f92c 100644 --- a/src/shared/services/settings.service.ts +++ b/src/shared/services/settings.service.ts @@ -1,6 +1,6 @@ +import { createSyncProxyForAsyncObject } from 'platform-bible-utils'; import dataProviderService from './data-provider.service'; import { - createSyncProxyForAsyncObject, ISettingsService, settingsServiceDataProviderName, settingsServiceObjectToProxy, @@ -14,7 +14,7 @@ async function initialize(): Promise { const executor = async () => { try { const provider = await dataProviderService.get(settingsServiceDataProviderName); - if (!provider) throw new Error('Menu data service undefined'); + if (!provider) throw new Error('Settings service undefined'); dataProvider = provider; resolve(); } catch (error) { @@ -27,9 +27,9 @@ async function initialize(): Promise { return initializationPromise; } -const menuDataService = createSyncProxyForAsyncObject(async () => { +const settingsService = createSyncProxyForAsyncObject(async () => { await initialize(); return dataProvider; }, settingsServiceObjectToProxy); -export default menuDataService; +export default settingsService; From 4bc18de276eda1ec5e0cdd94cc83249ca45a9d02 Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Tue, 6 Feb 2024 16:26:13 -0500 Subject: [PATCH 05/19] Update papi --- lib/papi-dts/papi.d.ts | 5 +++-- src/renderer/services/papi-frontend.service.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index 0c21bb8b19..e817485c8c 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -3884,6 +3884,7 @@ declare module "renderer/services/papi-frontend.service" { import { DataProviderService } from "shared/services/data-provider.service"; import { ProjectLookupServiceType } from "shared/services/project-lookup.service-model"; import { PapiFrontendProjectDataProviderService } from "shared/services/project-data-provider.service"; + import { ISettingsService } from "shared/services/settings.service-model"; import { DialogService } from "shared/services/dialog.service-model"; import * as papiReact from "renderer/services/papi-frontend-react.service"; import PapiRendererWebSocket from "renderer/services/renderer-web-socket.service"; @@ -3923,7 +3924,7 @@ declare module "renderer/services/papi-frontend.service" { */ react: typeof papiReact; /** JSDOC DESTINATION settingsService */ - settings: SettingsService; + settings: ISettingsService; /** JSDOC DESTINATION menuDataService */ menuData: IMenuDataService; }; @@ -3957,7 +3958,7 @@ declare module "renderer/services/papi-frontend.service" { /** JSDOC DESTINATION papiReact */ export const react: typeof papiReact; /** JSDOC DESTINATION settingsService */ - export const settings: SettingsService; + export const settings: ISettingsService; /** JSDOC DESTINATION menuDataService */ export const menuData: IMenuDataService; export type Papi = typeof papi; diff --git a/src/renderer/services/papi-frontend.service.ts b/src/renderer/services/papi-frontend.service.ts index bc6cd511c3..54be3d85ab 100644 --- a/src/renderer/services/papi-frontend.service.ts +++ b/src/renderer/services/papi-frontend.service.ts @@ -17,7 +17,8 @@ import { papiFrontendProjectDataProviderService, PapiFrontendProjectDataProviderService, } from '@shared/services/project-data-provider.service'; -import settingsService, { SettingsService } from '@shared/services/settings.service'; +import settingsService from '@shared/services/settings.service'; +import { ISettingsService } from '@shared/services/settings.service-model'; import dialogService from '@shared/services/dialog.service'; import { DialogService } from '@shared/services/dialog.service-model'; import * as papiReact from '@renderer/services/papi-frontend-react.service'; @@ -72,7 +73,7 @@ const papi = { */ react: papiReact, /** JSDOC DESTINATION settingsService */ - settings: settingsService as SettingsService, + settings: settingsService as ISettingsService, /** JSDOC DESTINATION menuDataService */ menuData: menuDataService as IMenuDataService, }; From 1da3c05007f3f460ced511e32f80d61c7dccf3c3 Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Tue, 6 Feb 2024 17:16:06 -0500 Subject: [PATCH 06/19] Update use setting hook --- lib/papi-dts/papi.d.ts | 8193 +++++++++-------- .../hooks/papi-hooks/use-setting.hook.ts | 29 +- 2 files changed, 4588 insertions(+), 3634 deletions(-) diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index e817485c8c..e3675a1e7c 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -1,166 +1,249 @@ /// /// /// -declare module "shared/models/web-view.model" { - /** The type of code that defines a webview's content */ - export enum WebViewContentType { - /** - * This webview is a React webview. It must specify its component by setting it to - * `globalThis.webViewComponent` - */ - React = "react", - /** This webview is a raw HTML/JS/CSS webview. */ - HTML = "html", - /** - * This webview's content is fetched from the url specified (iframe `src` attribute). Note that - * webViews of this type cannot access the `papi` because they cannot be on the same origin as the - * parent window. - */ - URL = "url" - } - /** What type a WebView is. Each WebView definition must have a unique type. */ - export type WebViewType = string; - /** ID for a specific WebView. Each WebView has a unique ID */ - export type WebViewId = string; - /** Base WebView properties that all WebViews share */ - type WebViewDefinitionBase = { - /** What type of WebView this is. Unique to all other WebView definitions */ - webViewType: WebViewType; - /** Unique ID among webviews specific to this webview instance. */ - id: WebViewId; - /** The code for the WebView that papi puts into an iframe */ - content: string; - /** - * Url of image to show on the title bar of the tab - * - * Defaults to Platform.Bible logo - */ - iconUrl?: string; - /** Name of the tab for the WebView */ - title?: string; - /** Tooltip that is shown when hovering over the webview title */ - tooltip?: string; - /** General object to store unique state for this webview */ - state?: Record; - /** - * Whether to allow the WebView iframe to interact with its parent as a same-origin website. - * Setting this to true adds `allow-same-origin` to the WebView iframe's [sandbox attribute] - * (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox). Defaults to - * `true`. - * - * Setting this to false on an HTML or React WebView prevents the iframe from importing the `papi` - * and such and also prevents others from accessing its document. This could be useful when you - * need secure input from the user because other WebViews may be able to attach event listeners to - * your inputs if you are on the same origin. Setting this to `false` on HTML or React WebViews is - * a big security win, but it makes interacting with the platform more challenging in some ways. - * - * Setting this to false on a URL WebView prevents the iframe from accessing same-origin features - * on its host website like storage APIs (localstorage, cookies, etc) and such. This will likely - * break many websites. - * - * It is best practice to set this to `false` where possible. - * - * Note: Until we have a message-passing API for WebViews, there is currently no way to interact - * with the platform via a WebView with `allowSameOrigin: false`. - * - * WARNING: If your WebView accepts secure user input like passwords on HTML or React WebViews, - * you MUST set this to `false` or you will risk exposing that secure input to other extensions - * who could be phishing for it. - */ - allowSameOrigin?: boolean; - /** - * Whether to allow scripts to run in this iframe. Setting this to true adds `allow-scripts` to - * the WebView iframe's [sandbox attribute] - * (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox). Defaults to `true` - * for HTML and React WebViews and `false` for URL WebViews - * - * WARNING: Setting this to `true` increases the possibility of a security threat occurring. If it - * is not necessary to run scripts in your WebView, you should set this to `false` to reduce - * risk. - */ - allowScripts?: boolean; - /** - * **For HTML and React WebViews:** List of [Host or scheme - * values](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#hosts_values) - * to include in the [`frame-src` - * directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src) - * for this WebView's iframe content-security-policy. This allows iframes with `src` attributes - * matching these host values to be loaded in your WebView. You can only specify values starting - * with `papi-extension:` and `https:`; any others are ignored. Specifying urls in this array - * whitelists those urls so you can embed iframes with those urls as the `src` attribute. By - * default, no urls are available to be iframes. If you want to embed iframes with the `src` - * attribute in your webview, you must include them in this property. - * - * For example, if you specify `allowFrameSources: ['https://example.com/']`, you will be able to - * embed iframes with urls starting with `papi-extension:` and on the same host as - * `https://example.com/` - * - * If you plan on embedding any iframes in your WebView, it is best practice to list only the host - * values you need to function. The more you list, the higher the theoretical security risks. - * - * --- - * - * **For URL WebViews:** List of strings representing RegExp patterns (passed into [the RegExp - * constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp)) - * to match against the `content` url specified (using the - * [`test`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test) - * function) to determine whether this iframe will be allowed to load. Specifying urls in this - * array is essentially a security check to make sure the url you pass is one of the urls you - * intend it to be. By default, the url you specify in `content` will be accepted (you do not have - * to specify this unless you want to, but it is recommended in some scenarios). - * - * Note: URL WebViews must have `papi-extension:` or `https:` urls. This property does not - * override that requirement. - * - * For example, if you specify `allowFrameSources: ['^papi-extension:', - * '^https://example\\.com.*']`, only `papi-extension:` and `https://example.com` urls will be - * accepted. - * - * If your WebView url is a `const` string and cannot change for any reason, you do not need to - * specify this property. However, if your WebView url is dynamic and can change in any way, it is - * best practice to specify this property and to list only the urls you need for your URL WebView - * to function. The more you list, the higher the theoretical security risks. - */ - allowedFrameSources?: string[]; - }; - /** WebView representation using React */ - export type WebViewDefinitionReact = WebViewDefinitionBase & { - /** Indicates this WebView uses React */ - contentType?: WebViewContentType.React; - /** String of styles to be loaded into the iframe for this WebView */ - styles?: string; - }; - /** WebView representation using HTML */ - export type WebViewDefinitionHtml = WebViewDefinitionBase & { - /** Indicates this WebView uses HTML */ - contentType: WebViewContentType.HTML; - }; - /** - * WebView representation using a URL. - * - * Note: you can only use `papi-extension:` and `https:` urls - */ - export type WebViewDefinitionURL = WebViewDefinitionBase & { - /** Indicates this WebView uses a URL */ - contentType: WebViewContentType.URL; - }; - /** Properties defining a type of WebView created by extensions to show web content */ - export type WebViewDefinition = WebViewDefinitionReact | WebViewDefinitionHtml | WebViewDefinitionURL; +declare module 'shared/models/web-view.model' { + /** The type of code that defines a webview's content */ + export enum WebViewContentType { + /** + * This webview is a React webview. It must specify its component by setting it to + * `globalThis.webViewComponent` + */ + React = 'react', + /** This webview is a raw HTML/JS/CSS webview. */ + HTML = 'html', + /** + * This webview's content is fetched from the url specified (iframe `src` attribute). Note that + * webViews of this type cannot access the `papi` because they cannot be on the same origin as the + * parent window. + */ + URL = 'url', + } + /** What type a WebView is. Each WebView definition must have a unique type. */ + export type WebViewType = string; + /** ID for a specific WebView. Each WebView has a unique ID */ + export type WebViewId = string; + /** Base WebView properties that all WebViews share */ + type WebViewDefinitionBase = { + /** What type of WebView this is. Unique to all other WebView definitions */ + webViewType: WebViewType; + /** Unique ID among webviews specific to this webview instance. */ + id: WebViewId; + /** The code for the WebView that papi puts into an iframe */ + content: string; + /** + * Url of image to show on the title bar of the tab + * + * Defaults to Platform.Bible logo + */ + iconUrl?: string; + /** Name of the tab for the WebView */ + title?: string; + /** Tooltip that is shown when hovering over the webview title */ + tooltip?: string; + /** General object to store unique state for this webview */ + state?: Record; + /** + * Whether to allow the WebView iframe to interact with its parent as a same-origin website. + * Setting this to true adds `allow-same-origin` to the WebView iframe's [sandbox attribute] + * (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox). Defaults to + * `true`. + * + * Setting this to false on an HTML or React WebView prevents the iframe from importing the `papi` + * and such and also prevents others from accessing its document. This could be useful when you + * need secure input from the user because other WebViews may be able to attach event listeners to + * your inputs if you are on the same origin. Setting this to `false` on HTML or React WebViews is + * a big security win, but it makes interacting with the platform more challenging in some ways. + * + * Setting this to false on a URL WebView prevents the iframe from accessing same-origin features + * on its host website like storage APIs (localstorage, cookies, etc) and such. This will likely + * break many websites. + * + * It is best practice to set this to `false` where possible. + * + * Note: Until we have a message-passing API for WebViews, there is currently no way to interact + * with the platform via a WebView with `allowSameOrigin: false`. + * + * WARNING: If your WebView accepts secure user input like passwords on HTML or React WebViews, + * you MUST set this to `false` or you will risk exposing that secure input to other extensions + * who could be phishing for it. + */ + allowSameOrigin?: boolean; + /** + * Whether to allow scripts to run in this iframe. Setting this to true adds `allow-scripts` to + * the WebView iframe's [sandbox attribute] + * (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox). Defaults to `true` + * for HTML and React WebViews and `false` for URL WebViews + * + * WARNING: Setting this to `true` increases the possibility of a security threat occurring. If it + * is not necessary to run scripts in your WebView, you should set this to `false` to reduce + * risk. + */ + allowScripts?: boolean; + /** + * **For HTML and React WebViews:** List of [Host or scheme + * values](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#hosts_values) + * to include in the [`frame-src` + * directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src) + * for this WebView's iframe content-security-policy. This allows iframes with `src` attributes + * matching these host values to be loaded in your WebView. You can only specify values starting + * with `papi-extension:` and `https:`; any others are ignored. Specifying urls in this array + * whitelists those urls so you can embed iframes with those urls as the `src` attribute. By + * default, no urls are available to be iframes. If you want to embed iframes with the `src` + * attribute in your webview, you must include them in this property. + * + * For example, if you specify `allowFrameSources: ['https://example.com/']`, you will be able to + * embed iframes with urls starting with `papi-extension:` and on the same host as + * `https://example.com/` + * + * If you plan on embedding any iframes in your WebView, it is best practice to list only the host + * values you need to function. The more you list, the higher the theoretical security risks. + * + * --- + * + * **For URL WebViews:** List of strings representing RegExp patterns (passed into [the RegExp + * constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp)) + * to match against the `content` url specified (using the + * [`test`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test) + * function) to determine whether this iframe will be allowed to load. Specifying urls in this + * array is essentially a security check to make sure the url you pass is one of the urls you + * intend it to be. By default, the url you specify in `content` will be accepted (you do not have + * to specify this unless you want to, but it is recommended in some scenarios). + * + * Note: URL WebViews must have `papi-extension:` or `https:` urls. This property does not + * override that requirement. + * + * For example, if you specify `allowFrameSources: ['^papi-extension:', + * '^https://example\\.com.*']`, only `papi-extension:` and `https://example.com` urls will be + * accepted. + * + * If your WebView url is a `const` string and cannot change for any reason, you do not need to + * specify this property. However, if your WebView url is dynamic and can change in any way, it is + * best practice to specify this property and to list only the urls you need for your URL WebView + * to function. The more you list, the higher the theoretical security risks. + */ + allowedFrameSources?: string[]; + }; + /** WebView representation using React */ + export type WebViewDefinitionReact = WebViewDefinitionBase & { + /** Indicates this WebView uses React */ + contentType?: WebViewContentType.React; + /** String of styles to be loaded into the iframe for this WebView */ + styles?: string; + }; + /** WebView representation using HTML */ + export type WebViewDefinitionHtml = WebViewDefinitionBase & { + /** Indicates this WebView uses HTML */ + contentType: WebViewContentType.HTML; + }; + /** + * WebView representation using a URL. + * + * Note: you can only use `papi-extension:` and `https:` urls + */ + export type WebViewDefinitionURL = WebViewDefinitionBase & { + /** Indicates this WebView uses a URL */ + contentType: WebViewContentType.URL; + }; + /** Properties defining a type of WebView created by extensions to show web content */ + export type WebViewDefinition = + | WebViewDefinitionReact + | WebViewDefinitionHtml + | WebViewDefinitionURL; + /** + * Saved WebView information that does not contain the actual content of the WebView. Saved into + * layouts. Could have as little as the type and ID. WebView providers load these into actual + * {@link WebViewDefinition}s and verify any existing properties on the WebViews. + */ + export type SavedWebViewDefinition = ( + | Partial> + | Partial> + | Partial> + ) & + Pick; + /** The properties on a WebViewDefinition that may be updated when that webview is already displayed */ + export type WebViewDefinitionUpdatableProperties = Pick< + WebViewDefinitionBase, + 'iconUrl' | 'title' | 'tooltip' + >; + /** + * WebViewDefinition properties for updating a WebView that is already displayed. Any unspecified + * properties will stay the same + */ + export type WebViewDefinitionUpdateInfo = Partial; + /** + * + * A React hook for working with a state object tied to a webview. Returns a WebView state value and + * a function to set it. Use similarly to `useState`. + * + * Only used in WebView iframes. + * + * _@param_ `stateKey` Key of the state value to use. The webview state holds a unique value per + * key. + * + * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be + * updated every render + * + * _@param_ `defaultStateValue` Value to use if the web view state didn't contain a value for the + * given 'stateKey' + * + * Note: this parameter is internally assigned to a `ref`, so changing it will not cause any hooks + * to re-run with its new value. Running `resetWebViewState()` will always update the state value + * returned to the latest `defaultStateValue`, and changing the `stateKey` will use the latest + * `defaultStateValue`. However, if `defaultStateValue` is changed while a state is + * `defaultStateValue` (meaning it is reset and has no value), the returned state value will not be + * updated to the new `defaultStateValue`. + * + * _@returns_ `[stateValue, setStateValue, resetWebViewState]` + * + * - `webViewStateValue`: The current value for the web view state at the key specified or + * `defaultStateValue` if a state was not found + * - `setWebViewState`: Function to use to update the web view state value at the key specified + * - `resetWebViewState`: Function that removes the web view state and resets the value to + * `defaultStateValue` + * + * _@example_ + * + * ```typescript + * const [lastPersonSeen, setLastPersonSeen] = useWebViewState('lastSeen', 'No one'); + * ``` + */ + export type UseWebViewStateHook = ( + stateKey: string, + defaultStateValue: T, + ) => [ + webViewStateValue: T, + setWebViewState: (stateValue: T) => void, + resetWebViewState: () => void, + ]; + /** + * + * Gets the updatable properties on this WebView's WebView definition + * + * _@returns_ updatable properties this WebView's WebView definition or undefined if not found for + * some reason + */ + export type GetWebViewDefinitionUpdatableProperties = () => + | WebViewDefinitionUpdatableProperties + | undefined; + /** + * + * Updates this WebView with the specified properties + * + * _@param_ `updateInfo` properties to update on the WebView. Any unspecified properties will stay + * the same + * + * _@returns_ true if successfully found the WebView to update; false otherwise + * + * _@example_ + * + * ```typescript + * updateWebViewDefinition({ title: `Hello ${name}` }); + * ``` + */ + export type UpdateWebViewDefinition = (updateInfo: WebViewDefinitionUpdateInfo) => boolean; + /** Props that are passed into the web view itself inside the iframe in the web view tab component */ + export type WebViewProps = { /** - * Saved WebView information that does not contain the actual content of the WebView. Saved into - * layouts. Could have as little as the type and ID. WebView providers load these into actual - * {@link WebViewDefinition}s and verify any existing properties on the WebViews. - */ - export type SavedWebViewDefinition = (Partial> | Partial> | Partial>) & Pick; - /** The properties on a WebViewDefinition that may be updated when that webview is already displayed */ - export type WebViewDefinitionUpdatableProperties = Pick; - /** - * WebViewDefinition properties for updating a WebView that is already displayed. Any unspecified - * properties will stay the same - */ - export type WebViewDefinitionUpdateInfo = Partial; - /** - * JSDOC SOURCE UseWebViewStateHook * * A React hook for working with a state object tied to a webview. Returns a WebView state value and * a function to set it. Use similarly to `useState`. @@ -197,22 +280,16 @@ declare module "shared/models/web-view.model" { * const [lastPersonSeen, setLastPersonSeen] = useWebViewState('lastSeen', 'No one'); * ``` */ - export type UseWebViewStateHook = (stateKey: string, defaultStateValue: T) => [ - webViewStateValue: T, - setWebViewState: (stateValue: T) => void, - resetWebViewState: () => void - ]; + useWebViewState: UseWebViewStateHook; /** - * JSDOC SOURCE GetWebViewDefinitionUpdatableProperties * * Gets the updatable properties on this WebView's WebView definition * * _@returns_ updatable properties this WebView's WebView definition or undefined if not found for * some reason */ - export type GetWebViewDefinitionUpdatableProperties = () => WebViewDefinitionUpdatableProperties | undefined; + getWebViewDefinitionUpdatableProperties: GetWebViewDefinitionUpdatableProperties; /** - * JSDOC SOURCE UpdateWebViewDefinition * * Updates this WebView with the specified properties * @@ -227,795 +304,434 @@ declare module "shared/models/web-view.model" { * updateWebViewDefinition({ title: `Hello ${name}` }); * ``` */ - export type UpdateWebViewDefinition = (updateInfo: WebViewDefinitionUpdateInfo) => boolean; - /** Props that are passed into the web view itself inside the iframe in the web view tab component */ - export type WebViewProps = { - /** JSDOC DESTINATION UseWebViewStateHook */ - useWebViewState: UseWebViewStateHook; - /** JSDOC DESTINATION GetWebViewDefinitionUpdatableProperties */ - getWebViewDefinitionUpdatableProperties: GetWebViewDefinitionUpdatableProperties; - /** JSDOC DESTINATION UpdateWebViewDefinition */ - updateWebViewDefinition: UpdateWebViewDefinition; - }; - /** Options that affect what `webViews.getWebView` does */ - export type GetWebViewOptions = { - /** - * If provided and if a web view with this ID exists, requests from the web view provider an - * existing WebView with this ID if one exists. The web view provider can deny the request if it - * chooses to do so. - * - * Alternatively, set this to '?' to attempt to find any existing web view with the specified - * webViewType. - * - * Note: setting `existingId` to `undefined` counts as providing in this case (providing is tested - * with `'existingId' in options`, not just testing if `existingId` is truthy). Not providing an - * `existingId` at all is the only way to specify we are not looking for an existing webView - */ - existingId?: string | '?' | undefined; - /** - * Whether to create a webview with a new ID and a webview with ID `existingId` was not found. - * Only relevant if `existingId` is provided. If `existingId` is not provided, this property is - * ignored. - * - * Defaults to true - */ - createNewIfNotFound?: boolean; - }; -} -declare module "shared/global-this.model" { - import { LogLevel } from 'electron-log'; - import { FunctionComponent } from 'react'; - import { GetWebViewDefinitionUpdatableProperties, UpdateWebViewDefinition, UseWebViewStateHook, WebViewDefinitionUpdatableProperties, WebViewDefinitionUpdateInfo, WebViewProps } from "shared/models/web-view.model"; - /** - * Variables that are defined in global scope. These must be defined in main.ts (main), index.ts - * (renderer), and extension-host.ts (extension host) - */ - global { - /** Type of process this is. Helps with running specific code based on which process you're in */ - var processType: ProcessType; - /** Whether this process is packaged or running from sources */ - var isPackaged: boolean; - /** - * Path to the app's resources directory. This is a string representation of the resources uri on - * frontend - */ - var resourcesPath: string; - /** How much logging should be recorded. Defaults to 'debug' if not packaged, 'info' if packaged */ - var logLevel: LogLevel; - /** - * A function that each React WebView extension must provide for Paranext to display it. Only used - * in WebView iframes. - */ - var webViewComponent: FunctionComponent; - /** JSDOC DESTINATION UseWebViewStateHook */ - var useWebViewState: UseWebViewStateHook; - /** - * Retrieve the value from web view state with the given 'stateKey', if it exists. Otherwise - * return default value - */ - var getWebViewState: (stateKey: string, defaultValue: T) => T; - /** Set the value for a given key in the web view state. */ - var setWebViewState: (stateKey: string, stateValue: T) => void; - /** Remove the value for a given key in the web view state */ - var resetWebViewState: (stateKey: string) => void; - var getWebViewDefinitionUpdatablePropertiesById: (webViewId: string) => WebViewDefinitionUpdatableProperties | undefined; - var updateWebViewDefinitionById: (webViewId: string, webViewDefinitionUpdateInfo: WebViewDefinitionUpdateInfo) => boolean; - /** JSDOC DESTINATION GetWebViewDefinitionUpdatableProperties */ - var getWebViewDefinitionUpdatableProperties: GetWebViewDefinitionUpdatableProperties; - /** JSDOC DESTINATION UpdateWebViewDefinition */ - var updateWebViewDefinition: UpdateWebViewDefinition; - } - /** Type of Paranext process */ - export enum ProcessType { - Main = "main", - Renderer = "renderer", - ExtensionHost = "extension-host" - } -} -declare module "shared/utils/util" { - import { ProcessType } from "shared/global-this.model"; - import { UnsubscriberAsync } from 'platform-bible-utils'; - /** - * Create a nonce that is at least 128 bits long and should be (is not currently) cryptographically - * random. See nonce spec at https://w3c.github.io/webappsec-csp/#security-nonces - * - * WARNING: THIS IS NOT CURRENTLY CRYPTOGRAPHICALLY SECURE! TODO: Make this cryptographically - * random! Use some polymorphic library that works in all contexts? - * https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues only works in browser - */ - export function newNonce(): string; + updateWebViewDefinition: UpdateWebViewDefinition; + }; + /** Options that affect what `webViews.getWebView` does */ + export type GetWebViewOptions = { /** - * Creates a safe version of a register function that returns a Promise. + * If provided and if a web view with this ID exists, requests from the web view provider an + * existing WebView with this ID if one exists. The web view provider can deny the request if it + * chooses to do so. * - * @param unsafeRegisterFn Function that does some kind of async registration and returns an - * unsubscriber and a promise that resolves when the registration is finished - * @param isInitialized Whether the service associated with this safe UnsubscriberAsync function is - * initialized - * @param initialize Promise that resolves when the service is finished initializing - * @returns Safe version of an unsafe function that returns a promise to an UnsubscriberAsync - * (meaning it will wait to register until the service is initialized) - */ - export const createSafeRegisterFn: (unsafeRegisterFn: (...args: TParam) => Promise, isInitialized: boolean, initialize: () => Promise) => (...args: TParam) => Promise; - /** - * Type of object passed to a complex request handler that provides information about the request. - * This type is used as the public-facing interface for requests - */ - export type ComplexRequest = { - /** The one who sent the request */ - senderId: number; - contents: TParam; - }; - type ComplexResponseSuccess = { - /** Whether the handler that created this response was successful in handling the request */ - success: true; - /** - * Content with which to respond to the request. Must be provided unless the response failed or - * TReturn is undefined - */ - contents: TReturn; - }; - type ComplexResponseFailure = { - /** Whether the handler that created this response was successful in handling the request */ - success: false; - /** - * Content with which to respond to the request. Must be provided unless the response failed or - * TReturn is undefined Removed from failure so we do not change the type of contents for type - * safety. We could add errorContents one day if we really need it - */ - /** Error explaining the problem that is only populated if success is false */ - errorMessage: string; - }; - /** - * Type of object to create when handling a complex request where you desire to provide additional - * information beyond the contents of the response This type is used as the public-facing interface - * for responses - */ - export type ComplexResponse = ComplexResponseSuccess | ComplexResponseFailure; - /** Type of request handler - indicates what type of parameters and what return type the handler has */ - export enum RequestHandlerType { - Args = "args", - Contents = "contents", - Complex = "complex" - } - /** - * Modules that someone might try to require in their extensions that we have similar apis for. When - * an extension requires these modules, an error throws that lets them know about our similar api. - */ - export const MODULE_SIMILAR_APIS: Readonly<{ - [moduleName: string]: string | { - [process in ProcessType | 'default']?: string; - } | undefined; - }>; - /** - * Get a message that says the module import was rejected and to try a similar api if available. - * - * @param moduleName Name of `require`d module that was rejected - * @returns String that says the import was rejected and a similar api to try - */ - export function getModuleSimilarApiMessage(moduleName: string): string; - /** Separator between parts of a serialized request */ - const REQUEST_TYPE_SEPARATOR = ":"; - /** Information about a request that tells us what to do with it */ - export type RequestType = { - /** The general category of request */ - category: string; - /** Specific identifier for this type of request */ - directive: string; - }; - /** - * String version of a request type that tells us what to do with a request. + * Alternatively, set this to '?' to attempt to find any existing web view with the specified + * webViewType. * - * Consists of two strings concatenated by a colon + * Note: setting `existingId` to `undefined` counts as providing in this case (providing is tested + * with `'existingId' in options`, not just testing if `existingId` is truthy). Not providing an + * `existingId` at all is the only way to specify we are not looking for an existing webView */ - export type SerializedRequestType = `${string}${typeof REQUEST_TYPE_SEPARATOR}${string}`; + existingId?: string | '?' | undefined; /** - * Create a request message requestType string from a category and a directive + * Whether to create a webview with a new ID and a webview with ID `existingId` was not found. + * Only relevant if `existingId` is provided. If `existingId` is not provided, this property is + * ignored. * - * @param category The general category of request - * @param directive Specific identifier for this type of request - * @returns Full requestType for use in network calls + * Defaults to true */ - export function serializeRequestType(category: string, directive: string): SerializedRequestType; - /** Split a request message requestType string into its parts */ - export function deserializeRequestType(requestType: SerializedRequestType): RequestType; + createNewIfNotFound?: boolean; + }; } -declare module "shared/data/internal-connection.model" { - /** - * Types that are internal to the communication we do through WebSocket. These types should not need - * to be used outside of NetworkConnectors and ConnectionService.ts - */ - import { ComplexRequest, ComplexResponse, SerializedRequestType } from "shared/utils/util"; - /** Represents when the client id has not been assigned by the server */ - export const CLIENT_ID_UNASSIGNED = -1; - /** "Client id" for the server */ - export const CLIENT_ID_SERVER = 0; - /** Represents when the connector info has not been populated by the server */ - export const CONNECTOR_INFO_DISCONNECTED: Readonly<{ - clientId: -1; - }>; - /** Prefix on requests that indicates that the request is a command */ - export const CATEGORY_COMMAND = "command"; - /** Information about the network connector */ - export type NetworkConnectorInfo = Readonly<{ - clientId: number; - }>; - /** Event emitted when client connections are established */ - export type ClientConnectEvent = { - clientId: number; - didReconnect: boolean; - }; - /** Event emitted when client connections are lost */ - export type ClientDisconnectEvent = { - clientId: number; - }; +declare module 'shared/global-this.model' { + import { LogLevel } from 'electron-log'; + import { FunctionComponent } from 'react'; + import { + GetWebViewDefinitionUpdatableProperties, + UpdateWebViewDefinition, + UseWebViewStateHook, + WebViewDefinitionUpdatableProperties, + WebViewDefinitionUpdateInfo, + WebViewProps, + } from 'shared/models/web-view.model'; + /** + * Variables that are defined in global scope. These must be defined in main.ts (main), index.ts + * (renderer), and extension-host.ts (extension host) + */ + global { + /** Type of process this is. Helps with running specific code based on which process you're in */ + var processType: ProcessType; + /** Whether this process is packaged or running from sources */ + var isPackaged: boolean; + /** + * Path to the app's resources directory. This is a string representation of the resources uri on + * frontend + */ + var resourcesPath: string; + /** How much logging should be recorded. Defaults to 'debug' if not packaged, 'info' if packaged */ + var logLevel: LogLevel; + /** + * A function that each React WebView extension must provide for Paranext to display it. Only used + * in WebView iframes. + */ + var webViewComponent: FunctionComponent; /** - * Functions that run when network connector events occur. These should likely be emit functions - * from NetworkEventEmitters so the events inform all interested connections + * + * A React hook for working with a state object tied to a webview. Returns a WebView state value and + * a function to set it. Use similarly to `useState`. + * + * Only used in WebView iframes. + * + * _@param_ `stateKey` Key of the state value to use. The webview state holds a unique value per + * key. + * + * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be + * updated every render + * + * _@param_ `defaultStateValue` Value to use if the web view state didn't contain a value for the + * given 'stateKey' + * + * Note: this parameter is internally assigned to a `ref`, so changing it will not cause any hooks + * to re-run with its new value. Running `resetWebViewState()` will always update the state value + * returned to the latest `defaultStateValue`, and changing the `stateKey` will use the latest + * `defaultStateValue`. However, if `defaultStateValue` is changed while a state is + * `defaultStateValue` (meaning it is reset and has no value), the returned state value will not be + * updated to the new `defaultStateValue`. + * + * _@returns_ `[stateValue, setStateValue, resetWebViewState]` + * + * - `webViewStateValue`: The current value for the web view state at the key specified or + * `defaultStateValue` if a state was not found + * - `setWebViewState`: Function to use to update the web view state value at the key specified + * - `resetWebViewState`: Function that removes the web view state and resets the value to + * `defaultStateValue` + * + * _@example_ + * + * ```typescript + * const [lastPersonSeen, setLastPersonSeen] = useWebViewState('lastSeen', 'No one'); + * ``` */ - export type NetworkConnectorEventHandlers = { - /** Handles when a new connection is established */ - didClientConnectHandler?: (event: ClientConnectEvent) => void; - /** Handles when a client disconnects */ - didClientDisconnectHandler?: (event: ClientDisconnectEvent) => void; - }; + var useWebViewState: UseWebViewStateHook; /** - * Whether this connector is setting up or has finished setting up its connection and is ready to - * communicate on the network + * Retrieve the value from web view state with the given 'stateKey', if it exists. Otherwise + * return default value */ - export enum ConnectionStatus { - /** This connector is not connected to the network */ - Disconnected = 0, - /** This connector is attempting to connect to the network and retrieve connectorInfo */ - Connecting = 1, - /** This connector has finished setting up its connection - has connectorInfo and such */ - Connected = 2 - } - /** Request to do something and to respond */ - export type InternalRequest = { - requestId: number; - } & ComplexRequest; - /** Response to a request */ - export type InternalResponse = { - /** The process that sent this Response */ - senderId: number; - requestId: number; - /** The process that originally sent the Request that matches to this response */ - requesterId: number; - } & ComplexResponse; - /** - * Handler for requests from the server. Used internally between network connector and Connection - * Service - */ - export type InternalRequestHandler = (requestType: string, request: InternalRequest) => Promise>; - /** Handler for requests from the server */ - export type RequestHandler = (requestType: SerializedRequestType, request: ComplexRequest) => Promise>; - /** Function that returns a clientId to which to send the request based on the requestType */ - export type RequestRouter = (requestType: string) => number; - /** Event to be sent out throughout all processes */ - export type InternalEvent = { - /** The process that emitted this Event */ - senderId: number; - /** Contents of the event */ - event: T; - }; + var getWebViewState: (stateKey: string, defaultValue: T) => T; + /** Set the value for a given key in the web view state. */ + var setWebViewState: (stateKey: string, stateValue: T) => void; + /** Remove the value for a given key in the web view state */ + var resetWebViewState: (stateKey: string) => void; + var getWebViewDefinitionUpdatablePropertiesById: ( + webViewId: string, + ) => WebViewDefinitionUpdatableProperties | undefined; + var updateWebViewDefinitionById: ( + webViewId: string, + webViewDefinitionUpdateInfo: WebViewDefinitionUpdateInfo, + ) => boolean; /** - * Handler for events from on the network. Used internally between network connector and Connection - * Service - */ - export type InternalNetworkEventHandler = (eventType: string, incomingEvent: InternalEvent) => void; - /** Handler for events from on the network */ - export type NetworkEventHandler = (eventType: string, event: T) => void; -} -declare module "shared/services/network-connector.interface" { - import { ConnectionStatus, InternalEvent, InternalNetworkEventHandler, InternalRequestHandler, NetworkConnectorEventHandlers, NetworkConnectorInfo, RequestRouter } from "shared/data/internal-connection.model"; - /** - * Interface that defines the network connection functionality the server and the client must - * implement. Used by NetworkConnectorFactory to supply the right kind of NetworkConnector to - * ConnectionService - */ - export default interface INetworkConnector { - /** Information about the connector. Populated by the server while connecting */ - connectorInfo: NetworkConnectorInfo; - /** - * Whether this connector is setting up or has finished setting up its connection and is ready to - * communicate on the network - */ - connectionStatus: ConnectionStatus; - /** - * Sets up the NetworkConnector by populating connector info, setting up event handlers, and doing - * one of the following: - * - * - On Client: connecting to the server. - * - On Server: opening an endpoint for clients to connect. - * - * MUST ALSO RUN notifyClientConnected() WHEN PROMISE RESOLVES - * - * @param localRequestHandler Function that handles requests from the connection. Only called when - * this connector can handle the request - * @param requestRouter Function that returns a clientId to which to send the request based on the - * requestType. If requestRouter returns this connector's clientId, localRequestHandler is used - * @param localEventHandler Function that handles events from the server by accepting an eventType - * and an event and emitting the event locally - * @param networkConnectorEventHandlers Functions that run when network connector events occur - * like when clients are disconnected - * @returns Promise that resolves with connector info when finished connecting - */ - connect: (localRequestHandler: InternalRequestHandler, requestRouter: RequestRouter, localEventHandler: InternalNetworkEventHandler, networkConnectorEventHandlers: NetworkConnectorEventHandlers) => Promise; - /** - * Notify the server that this client has received its connectorInfo and is ready to go. - * - * MUST RUN AFTER connect() WHEN ITS PROMISE RESOLVES - * - * TODO: Is this necessary? - */ - notifyClientConnected: () => Promise; - /** - * Disconnects from the connection: - * - * - On Client: disconnects from the server - * - On Server: disconnects from clients and closes its connection endpoint - */ - disconnect: () => void; - /** - * Send a request to the server/a client and resolve after receiving a response - * - * @param requestType The type of request - * @param contents Contents to send in the request - * @returns Promise that resolves with the response message - */ - request: InternalRequestHandler; - /** - * Sends an event to other processes. Does NOT run the local event subscriptions as they should be - * run by NetworkEventEmitter after sending on network. - * - * @param eventType Unique network event type for coordinating between processes - * @param event Event to emit on the network - */ - emitEventOnNetwork: (eventType: string, event: InternalEvent) => Promise; - } -} -declare module "shared/utils/internal-util" { - /** Utility functions specific to the internal technologies we are using. */ - import { ProcessType } from "shared/global-this.model"; - /** - * Determine if running on a client process (renderer, extension-host) or on the server. * - * @returns Returns true if running on a client, false otherwise - */ - export const isClient: () => boolean; - /** - * Determine if running on the server process (main) + * Gets the updatable properties on this WebView's WebView definition * - * @returns Returns true if running on the server, false otherwise + * _@returns_ updatable properties this WebView's WebView definition or undefined if not found for + * some reason */ - export const isServer: () => boolean; + var getWebViewDefinitionUpdatableProperties: GetWebViewDefinitionUpdatableProperties; /** - * Determine if running on the renderer process * - * @returns Returns true if running on the renderer, false otherwise - */ - export const isRenderer: () => boolean; - /** - * Determine if running on the extension host + * Updates this WebView with the specified properties * - * @returns Returns true if running on the extension host, false otherwise - */ - export const isExtensionHost: () => boolean; - /** - * Gets which kind of process this is (main, renderer, extension-host) + * _@param_ `updateInfo` properties to update on the WebView. Any unspecified properties will stay + * the same * - * @returns ProcessType for this process - */ - export const getProcessType: () => ProcessType; -} -declare module "shared/data/network-connector.model" { - /** - * Types that are relevant particularly to the implementation of communication on - * NetworkConnector.ts files Do not use these types outside of ClientNetworkConnector.ts and - * ServerNetworkConnector.ts - */ - import { InternalEvent, InternalRequest, InternalResponse, NetworkConnectorInfo } from "shared/data/internal-connection.model"; - /** Port to use for the webSocket */ - export const WEBSOCKET_PORT = 8876; - /** Number of attempts a client will make to connect to the WebSocket server before failing */ - export const WEBSOCKET_ATTEMPTS_MAX = 5; - /** - * Time in ms for the client to wait before attempting to connect to the WebSocket server again - * after a failure - */ - export const WEBSOCKET_ATTEMPTS_WAIT = 1000; - /** WebSocket message type that indicates how to handle it */ - export enum MessageType { - InitClient = "init-client", - ClientConnect = "client-connect", - Request = "request", - Response = "response", - Event = "event" - } - /** Message sent to the client to give it NetworkConnectorInfo */ - export type InitClient = { - type: MessageType.InitClient; - senderId: number; - connectorInfo: NetworkConnectorInfo; - /** Guid unique to this connection. Used to verify important messages like reconnecting */ - clientGuid: string; - }; - /** Message responding to the server to let it know this connection is ready to receive messages */ - export type ClientConnect = { - type: MessageType.ClientConnect; - senderId: number; - /** - * ClientGuid for this client the last time it was connected to the server. Used when reconnecting - * (like if the browser refreshes): if the server has a connection with this clientGuid, it will - * unregister all requests on that client so the reconnecting client can register its request - * handlers again. - */ - reconnectingClientGuid?: string; - }; - /** Request to do something and to respond */ - export type WebSocketRequest = { - type: MessageType.Request; - /** What kind of request this is. Certain command, etc */ - requestType: string; - } & InternalRequest; - /** Response to a request */ - export type WebSocketResponse = { - type: MessageType.Response; - /** What kind of request this is. Certain command, etc */ - requestType: string; - } & InternalResponse; - /** Event to be sent out throughout all processes */ - export type WebSocketEvent = { - type: MessageType.Event; - /** What kind of event this is */ - eventType: string; - } & InternalEvent; - /** Messages send by the WebSocket */ - export type Message = InitClient | ClientConnect | WebSocketRequest | WebSocketResponse | WebSocketEvent; -} -declare module "shared/services/logger.service" { - import log from 'electron-log'; - export const WARN_TAG = ""; - /** - * Format a string of a service message - * - * @param message Message from the service - * @param serviceName Name of the service to show in the log - * @param tag Optional tag at the end of the service name - * @returns Formatted string of a service message - */ - export function formatLog(message: string, serviceName: string, tag?: string): string; - /** - * JSDOC SOURCE logger + * _@returns_ true if successfully found the WebView to update; false otherwise * - * All extensions and services should use this logger to provide a unified output of logs + * _@example_ + * + * ```typescript + * updateWebViewDefinition({ title: `Hello ${name}` }); + * ``` */ - const logger: log.MainLogger & { - default: log.MainLogger; - }; - export default logger; + var updateWebViewDefinition: UpdateWebViewDefinition; + } + /** Type of Paranext process */ + export enum ProcessType { + Main = 'main', + Renderer = 'renderer', + ExtensionHost = 'extension-host', + } } -declare module "client/services/web-socket.interface" { +declare module 'shared/utils/util' { + import { ProcessType } from 'shared/global-this.model'; + import { UnsubscriberAsync } from 'platform-bible-utils'; + /** + * Create a nonce that is at least 128 bits long and should be (is not currently) cryptographically + * random. See nonce spec at https://w3c.github.io/webappsec-csp/#security-nonces + * + * WARNING: THIS IS NOT CURRENTLY CRYPTOGRAPHICALLY SECURE! TODO: Make this cryptographically + * random! Use some polymorphic library that works in all contexts? + * https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues only works in browser + */ + export function newNonce(): string; + /** + * Creates a safe version of a register function that returns a Promise. + * + * @param unsafeRegisterFn Function that does some kind of async registration and returns an + * unsubscriber and a promise that resolves when the registration is finished + * @param isInitialized Whether the service associated with this safe UnsubscriberAsync function is + * initialized + * @param initialize Promise that resolves when the service is finished initializing + * @returns Safe version of an unsafe function that returns a promise to an UnsubscriberAsync + * (meaning it will wait to register until the service is initialized) + */ + export const createSafeRegisterFn: ( + unsafeRegisterFn: (...args: TParam) => Promise, + isInitialized: boolean, + initialize: () => Promise, + ) => (...args: TParam) => Promise; + /** + * Type of object passed to a complex request handler that provides information about the request. + * This type is used as the public-facing interface for requests + */ + export type ComplexRequest = { + /** The one who sent the request */ + senderId: number; + contents: TParam; + }; + type ComplexResponseSuccess = { + /** Whether the handler that created this response was successful in handling the request */ + success: true; + /** + * Content with which to respond to the request. Must be provided unless the response failed or + * TReturn is undefined + */ + contents: TReturn; + }; + type ComplexResponseFailure = { + /** Whether the handler that created this response was successful in handling the request */ + success: false; + /** + * Content with which to respond to the request. Must be provided unless the response failed or + * TReturn is undefined Removed from failure so we do not change the type of contents for type + * safety. We could add errorContents one day if we really need it + */ + /** Error explaining the problem that is only populated if success is false */ + errorMessage: string; + }; + /** + * Type of object to create when handling a complex request where you desire to provide additional + * information beyond the contents of the response This type is used as the public-facing interface + * for responses + */ + export type ComplexResponse = + | ComplexResponseSuccess + | ComplexResponseFailure; + /** Type of request handler - indicates what type of parameters and what return type the handler has */ + export enum RequestHandlerType { + Args = 'args', + Contents = 'contents', + Complex = 'complex', + } + /** + * Modules that someone might try to require in their extensions that we have similar apis for. When + * an extension requires these modules, an error throws that lets them know about our similar api. + */ + export const MODULE_SIMILAR_APIS: Readonly<{ + [moduleName: string]: + | string + | { + [process in ProcessType | 'default']?: string; + } + | undefined; + }>; + /** + * Get a message that says the module import was rejected and to try a similar api if available. + * + * @param moduleName Name of `require`d module that was rejected + * @returns String that says the import was rejected and a similar api to try + */ + export function getModuleSimilarApiMessage(moduleName: string): string; + /** Separator between parts of a serialized request */ + const REQUEST_TYPE_SEPARATOR = ':'; + /** Information about a request that tells us what to do with it */ + export type RequestType = { + /** The general category of request */ + category: string; + /** Specific identifier for this type of request */ + directive: string; + }; + /** + * String version of a request type that tells us what to do with a request. + * + * Consists of two strings concatenated by a colon + */ + export type SerializedRequestType = `${string}${typeof REQUEST_TYPE_SEPARATOR}${string}`; + /** + * Create a request message requestType string from a category and a directive + * + * @param category The general category of request + * @param directive Specific identifier for this type of request + * @returns Full requestType for use in network calls + */ + export function serializeRequestType(category: string, directive: string): SerializedRequestType; + /** Split a request message requestType string into its parts */ + export function deserializeRequestType(requestType: SerializedRequestType): RequestType; +} +declare module 'shared/data/internal-connection.model' { + /** + * Types that are internal to the communication we do through WebSocket. These types should not need + * to be used outside of NetworkConnectors and ConnectionService.ts + */ + import { ComplexRequest, ComplexResponse, SerializedRequestType } from 'shared/utils/util'; + /** Represents when the client id has not been assigned by the server */ + export const CLIENT_ID_UNASSIGNED = -1; + /** "Client id" for the server */ + export const CLIENT_ID_SERVER = 0; + /** Represents when the connector info has not been populated by the server */ + export const CONNECTOR_INFO_DISCONNECTED: Readonly<{ + clientId: -1; + }>; + /** Prefix on requests that indicates that the request is a command */ + export const CATEGORY_COMMAND = 'command'; + /** Information about the network connector */ + export type NetworkConnectorInfo = Readonly<{ + clientId: number; + }>; + /** Event emitted when client connections are established */ + export type ClientConnectEvent = { + clientId: number; + didReconnect: boolean; + }; + /** Event emitted when client connections are lost */ + export type ClientDisconnectEvent = { + clientId: number; + }; + /** + * Functions that run when network connector events occur. These should likely be emit functions + * from NetworkEventEmitters so the events inform all interested connections + */ + export type NetworkConnectorEventHandlers = { + /** Handles when a new connection is established */ + didClientConnectHandler?: (event: ClientConnectEvent) => void; + /** Handles when a client disconnects */ + didClientDisconnectHandler?: (event: ClientDisconnectEvent) => void; + }; + /** + * Whether this connector is setting up or has finished setting up its connection and is ready to + * communicate on the network + */ + export enum ConnectionStatus { + /** This connector is not connected to the network */ + Disconnected = 0, + /** This connector is attempting to connect to the network and retrieve connectorInfo */ + Connecting = 1, + /** This connector has finished setting up its connection - has connectorInfo and such */ + Connected = 2, + } + /** Request to do something and to respond */ + export type InternalRequest = { + requestId: number; + } & ComplexRequest; + /** Response to a request */ + export type InternalResponse = { + /** The process that sent this Response */ + senderId: number; + requestId: number; + /** The process that originally sent the Request that matches to this response */ + requesterId: number; + } & ComplexResponse; + /** + * Handler for requests from the server. Used internally between network connector and Connection + * Service + */ + export type InternalRequestHandler = ( + requestType: string, + request: InternalRequest, + ) => Promise>; + /** Handler for requests from the server */ + export type RequestHandler = ( + requestType: SerializedRequestType, + request: ComplexRequest, + ) => Promise>; + /** Function that returns a clientId to which to send the request based on the requestType */ + export type RequestRouter = (requestType: string) => number; + /** Event to be sent out throughout all processes */ + export type InternalEvent = { + /** The process that emitted this Event */ + senderId: number; + /** Contents of the event */ + event: T; + }; + /** + * Handler for events from on the network. Used internally between network connector and Connection + * Service + */ + export type InternalNetworkEventHandler = ( + eventType: string, + incomingEvent: InternalEvent, + ) => void; + /** Handler for events from on the network */ + export type NetworkEventHandler = (eventType: string, event: T) => void; +} +declare module 'shared/services/network-connector.interface' { + import { + ConnectionStatus, + InternalEvent, + InternalNetworkEventHandler, + InternalRequestHandler, + NetworkConnectorEventHandlers, + NetworkConnectorInfo, + RequestRouter, + } from 'shared/data/internal-connection.model'; + /** + * Interface that defines the network connection functionality the server and the client must + * implement. Used by NetworkConnectorFactory to supply the right kind of NetworkConnector to + * ConnectionService + */ + export default interface INetworkConnector { + /** Information about the connector. Populated by the server while connecting */ + connectorInfo: NetworkConnectorInfo; /** - * Interface that defines the webSocket functionality the extension host and the renderer must - * implement. Used by WebSocketFactory to supply the right kind of WebSocket to - * ClientNetworkConnector. For now, we are just using the browser WebSocket type. We may need - * specific functionality that don't line up between the ws library's implementation and the browser - * implementation. We can adjust as needed at that point. + * Whether this connector is setting up or has finished setting up its connection and is ready to + * communicate on the network */ - export type IWebSocket = WebSocket; -} -declare module "renderer/services/renderer-web-socket.service" { - /** Once our network is running, run this to stop extensions from connecting to it directly */ - export const blockWebSocketsToPapiNetwork: () => void; + connectionStatus: ConnectionStatus; /** - * JSDOC SOURCE PapiRendererWebSocket This wraps the browser's WebSocket implementation to provide - * better control over internet access. It is isomorphic with the standard WebSocket, so it should - * act as a drop-in replacement. + * Sets up the NetworkConnector by populating connector info, setting up event handlers, and doing + * one of the following: * - * Note that the Node WebSocket implementation is different and not wrapped here. + * - On Client: connecting to the server. + * - On Server: opening an endpoint for clients to connect. + * + * MUST ALSO RUN notifyClientConnected() WHEN PROMISE RESOLVES + * + * @param localRequestHandler Function that handles requests from the connection. Only called when + * this connector can handle the request + * @param requestRouter Function that returns a clientId to which to send the request based on the + * requestType. If requestRouter returns this connector's clientId, localRequestHandler is used + * @param localEventHandler Function that handles events from the server by accepting an eventType + * and an event and emitting the event locally + * @param networkConnectorEventHandlers Functions that run when network connector events occur + * like when clients are disconnected + * @returns Promise that resolves with connector info when finished connecting */ - export default class PapiRendererWebSocket implements WebSocket { - readonly CONNECTING: 0; - readonly OPEN: 1; - readonly CLOSING: 2; - readonly CLOSED: 3; - addEventListener: (type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions) => void; - binaryType: BinaryType; - bufferedAmount: number; - close: (code?: number, reason?: string) => void; - dispatchEvent: (event: Event) => boolean; - extensions: string; - onclose: ((this: WebSocket, ev: CloseEvent) => any) | null; - onerror: ((this: WebSocket, ev: Event) => any) | null; - onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null; - onopen: ((this: WebSocket, ev: Event) => any) | null; - protocol: string; - readyState: number; - removeEventListener: (type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | EventListenerOptions) => void; - send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void; - url: string; - constructor(url: string | URL, protocols?: string | string[]); - } -} -declare module "extension-host/services/extension-host-web-socket.model" { - import ws from 'ws'; - /** - * Extension-host client uses ws as its WebSocket client, but the renderer can't use it. So we need - * to exclude it from the renderer webpack bundle like this. - */ - export default ws; -} -declare module "client/services/web-socket.factory" { - import { IWebSocket } from "client/services/web-socket.interface"; - /** - * Creates a WebSocket for the renderer or extension host depending on where you're running - * - * @returns WebSocket - */ - export const createWebSocket: (url: string) => Promise; -} -declare module "client/services/client-network-connector.service" { - import { ConnectionStatus, InternalEvent, InternalNetworkEventHandler, InternalRequest, InternalRequestHandler, InternalResponse, NetworkConnectorInfo, RequestRouter } from "shared/data/internal-connection.model"; - import INetworkConnector from "shared/services/network-connector.interface"; - /** Handles the connection from the client to the server */ - export default class ClientNetworkConnector implements INetworkConnector { - connectorInfo: NetworkConnectorInfo; - connectionStatus: ConnectionStatus; - /** The webSocket connected to the server */ - private webSocket?; - /** - * All message subscriptions - emitters that emit an event each time a message with a specific - * message type comes in - */ - private messageEmitters; - /** - * Promise that resolves when the connection is finished or rejects if disconnected before the - * connection finishes - */ - private connectPromise?; - /** Function that removes this initClient handler from the connection */ - private unsubscribeHandleInitClientMessage?; - /** Function that removes this response handler from the connection */ - private unsubscribeHandleResponseMessage?; - /** Function that removes this handleRequest from the connection */ - private unsubscribeHandleRequestMessage?; - /** Function that removes this handleEvent from the connection */ - private unsubscribeHandleEventMessage?; - /** - * Function to call when we receive a request that is registered on this connector. Handles - * requests from the connection and returns a response to send back - */ - private localRequestHandler?; - /** - * Function to call when we are sending a request. Returns a clientId to which to send the request - * based on the requestType - */ - private requestRouter?; - /** - * Function to call when we receive an event. Handles events from the connection by emitting the - * event locally - */ - private localEventHandler?; - /** All requests that are waiting for a response */ - private requests; - /** Unique Guid associated with this connection. Used to verify certain things with server */ - private clientGuid; - connect: (localRequestHandler: InternalRequestHandler, requestRouter: RequestRouter, localEventHandler: InternalNetworkEventHandler) => Promise>; - notifyClientConnected: () => Promise; - disconnect: () => void; - request: (requestType: string, request: InternalRequest) => Promise>; - emitEventOnNetwork: (eventType: string, event: InternalEvent) => Promise; - /** - * Send a message to the server via webSocket. Throws if not connected - * - * @param message Message to send - */ - private sendMessage; - /** - * Receives and appropriately publishes server webSocket messages - * - * @param event WebSocket message information - * @param fromSelf Whether this message is from this connector instead of from someone else - */ - private onMessage; - /** - * Subscribes a function to run on webSocket messages of a particular type - * - * @param messageType The type of message on which to subscribe the function - * @param callback Function to run with the contents of the webSocket message - * @returns Unsubscriber function to run to stop calling the passed-in function on webSocket - * messages - */ - private subscribe; - /** - * Function that handles webSocket messages of type Response. Resolves the request associated with - * the received response message - * - * @param response Response message to resolve - */ - private handleResponseMessage; - /** - * Function that handles incoming webSocket messages and locally sent messages of type Request. - * Runs the requestHandler provided in connect() and sends a message with the response - * - * @param requestMessage Request message to handle - * @param isIncoming Whether this message is coming from the server and we should definitely - * handle it locally or if it is a locally sent request and we should send to the server if we - * don't have a local handler - */ - private handleRequestMessage; - /** - * Function that handles incoming webSocket messages of type Event. Runs the eventHandler provided - * in connect() - * - * @param eventMessage Event message to handle - */ - private handleEventMessage; - } -} -declare module "main/services/server-network-connector.service" { - import { ConnectionStatus, InternalEvent, InternalNetworkEventHandler, InternalRequest, InternalRequestHandler, InternalResponse, NetworkConnectorEventHandlers, NetworkConnectorInfo, RequestRouter } from "shared/data/internal-connection.model"; - import INetworkConnector from "shared/services/network-connector.interface"; - /** Handles the endpoint and connections from the server to the clients */ - export default class ServerNetworkConnector implements INetworkConnector { - connectorInfo: NetworkConnectorInfo; - connectionStatus: ConnectionStatus; - /** The webSocket connected to the server */ - private webSocketServer?; - /** The next client id to use for a new connection. Starts at 1 because the server is 0 */ - private nextClientId; - /** The webSocket clients that are connected and information about them */ - private clientSockets; - /** - * All message subscriptions - emitters that emit an event each time a message with a specific - * message type comes in - */ - private messageEmitters; - /** - * Promise that resolves when finished starting the server or rejects if disconnected before the - * server finishes - */ - private connectPromise?; - /** Function that removes this clientConnect handler from connections */ - private unsubscribeHandleClientConnectMessage?; - /** Function that removes this response handler from connections */ - private unsubscribeHandleResponseMessage?; - /** Function that removes this handleRequest from connections */ - private unsubscribeHandleRequestMessage?; - /** Function that removes this handleEvent from the connection */ - private unsubscribeHandleEventMessage?; - /** - * Function to call when we receive a request that is registered on this connector. Handles - * requests from connections and returns a response to send back - */ - private localRequestHandler?; - /** - * Function to call when we are sending a request. Returns a clientId to which to send the request - * based on the requestType - */ - private requestRouter?; - /** - * Function to call when we receive an event. Handles events from connections and emits the event - * locally - */ - private localEventHandler?; - /** Functions to run when network connector events occur like when clients are disconnected */ - private networkConnectorEventHandlers?; - /** All requests that are waiting for a response */ - private requests; - connect: (localRequestHandler: InternalRequestHandler, requestRouter: RequestRouter, localEventHandler: InternalNetworkEventHandler, networkConnectorEventHandlers: NetworkConnectorEventHandlers) => Promise>; - notifyClientConnected: () => Promise; - disconnect: () => void; - request: (requestType: string, request: InternalRequest) => Promise>; - emitEventOnNetwork: (eventType: string, event: InternalEvent) => Promise; - /** Get the client socket for a certain clientId. Throws if not found */ - private getClientSocket; - /** - * Attempts to get the client socket for a certain clientGuid. Returns undefined if not found. - * This does not throw because it will likely be very common that we do not have a clientId for a - * certain clientGuid as connecting clients will often supply old clientGuids. - */ - private getClientSocketFromGuid; - /** Get the clientId for a certain webSocket. Throws if not found */ - private getClientIdFromSocket; - /** - * Send a message to a client via webSocket. Throws if not connected - * - * @param message Message to send - * @param recipientId The client to which to send the message. TODO: determine if we can intuit - * this instead - */ - private sendMessage; - /** - * Receives and appropriately publishes webSocket messages - * - * @param event WebSocket message information - * @param fromSelf Whether this message is from this connector instead of from someone else - */ - private onMessage; - /** - * Subscribes a function to run on webSocket messages of a particular type - * - * @param messageType The type of message on which to subscribe the function - * @param callback Function to run with the contents of the webSocket message - * @returns Unsubscriber function to run to stop calling the passed-in function on webSocket - * messages - */ - private subscribe; - /** - * Registers an incoming webSocket connection and sends connection info with InitClient. Does not - * consider the client fully connected yet until they respond and tell us they connected with - * ClientConnect - */ - private onClientConnect; - /** Handles when client connection disconnects. Unregisters and such */ - private onClientDisconnect; - /** Closes connection and unregisters a client webSocket when it has disconnected */ - private disconnectClient; - /** - * Function that handles webSocket messages of type ClientConnect. Mark the connection fully - * connected and notify that a client connected or reconnected - * - * @param clientConnect Message from the client about the connection - * @param connectorId ClientId of the client who is sending this ClientConnect message - */ - private handleClientConnectMessage; - /** - * Function that handles webSocket messages of type Response. Resolves the request associated with - * the received response message or forwards to appropriate client - * - * @param response Response message to resolve - * @param responderId Responding client - */ - private handleResponseMessage; - /** - * Function that handles incoming webSocket messages and locally sent messages of type Request. - * Handles the request and sends a response if we have a handler or forwards to the appropriate - * client - * - * @param requestMessage Request to handle - * @param requesterId Who sent this message - */ - private handleRequestMessage; - /** - * Function that handles incoming webSocket messages of type Event. Runs the eventHandler provided - * in connect() and forwards the event to other clients - * - * @param eventMessage Event message to handle - */ - private handleEventMessage; - } -} -declare module "shared/services/network-connector.factory" { - import INetworkConnector from "shared/services/network-connector.interface"; + connect: ( + localRequestHandler: InternalRequestHandler, + requestRouter: RequestRouter, + localEventHandler: InternalNetworkEventHandler, + networkConnectorEventHandlers: NetworkConnectorEventHandlers, + ) => Promise; /** - * Creates a NetworkConnector for the client or the server depending on where you're running + * Notify the server that this client has received its connectorInfo and is ready to go. + * + * MUST RUN AFTER connect() WHEN ITS PROMISE RESOLVES * - * @returns NetworkConnector + * TODO: Is this necessary? */ - export const createNetworkConnector: () => Promise; -} -declare module "shared/services/connection.service" { + notifyClientConnected: () => Promise; /** - * Handles setting up a connection to the electron backend and exchanging simple messages. Do not - * use outside NetworkService.ts. For communication, use NetworkService.ts as it is an abstraction - * over this. + * Disconnects from the connection: + * + * - On Client: disconnects from the server + * - On Server: disconnects from clients and closes its connection endpoint */ - import { NetworkConnectorEventHandlers, NetworkEventHandler, RequestHandler, RequestRouter } from "shared/data/internal-connection.model"; - import { ComplexResponse } from "shared/utils/util"; + disconnect: () => void; /** - * Send a request to the server and resolve after receiving a response + * Send a request to the server/a client and resolve after receiving a response * * @param requestType The type of request * @param contents Contents to send in the request * @returns Promise that resolves with the response message */ - export const request: (requestType: string, contents: TParam) => Promise>; + request: InternalRequestHandler; /** * Sends an event to other processes. Does NOT run the local event subscriptions as they should be * run by NetworkEventEmitter after sending on network. @@ -1023,582 +739,1251 @@ declare module "shared/services/connection.service" { * @param eventType Unique network event type for coordinating between processes * @param event Event to emit on the network */ - export const emitEventOnNetwork: (eventType: string, event: T) => Promise; - /** Disconnects from the server */ - export const disconnect: () => void; - /** - * Sets up the ConnectionService by connecting to the server and setting up event handlers - * - * @param localRequestHandler Function that handles requests from the server by accepting a - * requestType and a ComplexRequest and returning a Promise of a Complex Response - * @param networkRequestRouter Function that determines the appropriate clientId to which to send - * requests of the given type - * @param localEventHandler Function that handles events from the server by accepting an eventType - * and an event and emitting the event locally - * @param connectorEventHandlers Functions that run when network connector events occur like when - * clients are disconnected - * @returns Promise that resolves when finished connecting - */ - export const connect: (localRequestHandler: RequestHandler, networkRequestRouter: RequestRouter, localEventHandler: NetworkEventHandler, connectorEventHandlers: NetworkConnectorEventHandlers) => Promise; - /** Gets this connection's clientId */ - export const getClientId: () => number; -} -declare module "shared/models/papi-network-event-emitter.model" { - import { PlatformEventHandler, PlatformEventEmitter } from 'platform-bible-utils'; - /** - * Networked version of EventEmitter - accepts subscriptions to an event and runs the subscription - * callbacks when the event is emitted. Events on NetworkEventEmitters can be emitted across - * processes. They are coordinated between processes by their type. Use eventEmitter.event(callback) - * to subscribe to the event. Use eventEmitter.emit(event) to run the subscriptions. Generally, this - * EventEmitter should be private, and its event should be public. That way, the emitter is not - * publicized, but anyone can subscribe to the event. - * - * WARNING: Do not use this class directly outside of NetworkService, or it will not do what you - * expect. Use NetworkService.createNetworkEventEmitter. - * - * WARNING: You cannot emit events with complex types on the network. - */ - export default class PapiNetworkEventEmitter extends PlatformEventEmitter { - /** Callback that sends the event to other processes on the network when it is emitted */ - private networkSubscriber; - /** Callback that runs when the emitter is disposed - should handle unlinking from the network */ - private networkDisposer; - /** - * Creates a NetworkEventEmitter - * - * @param networkSubscriber Callback that accepts the event and emits it to other processes - * @param networkDisposer Callback that unlinks this emitter from the network - */ - constructor( - /** Callback that sends the event to other processes on the network when it is emitted */ - networkSubscriber: PlatformEventHandler, - /** Callback that runs when the emitter is disposed - should handle unlinking from the network */ - networkDisposer: () => void); - emit: (event: T) => void; - /** - * Runs only the subscriptions for the event that are on this process. Does not send over network - * - * @param event Event data to provide to subscribed callbacks - */ - emitLocal(event: T): void; - dispose: () => Promise; - } + emitEventOnNetwork: (eventType: string, event: InternalEvent) => Promise; + } } -declare module "shared/services/network.service" { - /** - * Handles requests, responses, subscriptions, etc. to the backend. Likely shouldn't need/want to - * expose this whole service on papi, but there are a few things that are exposed via - * papiNetworkService - */ - import { ClientConnectEvent, ClientDisconnectEvent } from "shared/data/internal-connection.model"; - import { UnsubscriberAsync, PlatformEventEmitter, PlatformEvent } from 'platform-bible-utils'; - import { ComplexRequest, ComplexResponse, RequestHandlerType, SerializedRequestType } from "shared/utils/util"; - /** - * Args handler function for a request. Called when a request is handled. The function should accept - * the spread of the contents array of the request as its parameters. The function should return an - * object that becomes the contents object of the response. This type of handler is a normal - * function. - */ - type ArgsRequestHandler = any[], TReturn = any> = (...args: TParam) => Promise | TReturn; - /** - * Contents handler function for a request. Called when a request is handled. The function should - * accept the contents object of the request as its single parameter. The function should return an - * object that becomes the contents object of the response. - */ - type ContentsRequestHandler = (contents: TParam) => Promise; - /** - * Complex handler function for a request. Called when a request is handled. The function should - * accept a ComplexRequest object as its single parameter. The function should return a - * ComplexResponse object that becomes the response.. This type of handler is the most flexible of - * the request handlers. - */ - type ComplexRequestHandler = (request: ComplexRequest) => Promise>; - /** Event that emits with clientId when a client connects */ - export const onDidClientConnect: PlatformEvent; - /** Event that emits with clientId when a client disconnects */ - export const onDidClientDisconnect: PlatformEvent; - /** Closes the network services gracefully */ - export const shutdown: () => void; - /** Sets up the NetworkService. Runs only once */ - export const initialize: () => Promise; - /** - * Send a request on the network and resolve the response contents. - * - * @param requestType The type of request - * @param args Arguments to send in the request (put in request.contents) - * @returns Promise that resolves with the response message - */ - export const request: (requestType: SerializedRequestType, ...args: TParam) => Promise; +declare module 'shared/utils/internal-util' { + /** Utility functions specific to the internal technologies we are using. */ + import { ProcessType } from 'shared/global-this.model'; + /** + * Determine if running on a client process (renderer, extension-host) or on the server. + * + * @returns Returns true if running on a client, false otherwise + */ + export const isClient: () => boolean; + /** + * Determine if running on the server process (main) + * + * @returns Returns true if running on the server, false otherwise + */ + export const isServer: () => boolean; + /** + * Determine if running on the renderer process + * + * @returns Returns true if running on the renderer, false otherwise + */ + export const isRenderer: () => boolean; + /** + * Determine if running on the extension host + * + * @returns Returns true if running on the extension host, false otherwise + */ + export const isExtensionHost: () => boolean; + /** + * Gets which kind of process this is (main, renderer, extension-host) + * + * @returns ProcessType for this process + */ + export const getProcessType: () => ProcessType; +} +declare module 'shared/data/network-connector.model' { + /** + * Types that are relevant particularly to the implementation of communication on + * NetworkConnector.ts files Do not use these types outside of ClientNetworkConnector.ts and + * ServerNetworkConnector.ts + */ + import { + InternalEvent, + InternalRequest, + InternalResponse, + NetworkConnectorInfo, + } from 'shared/data/internal-connection.model'; + /** Port to use for the webSocket */ + export const WEBSOCKET_PORT = 8876; + /** Number of attempts a client will make to connect to the WebSocket server before failing */ + export const WEBSOCKET_ATTEMPTS_MAX = 5; + /** + * Time in ms for the client to wait before attempting to connect to the WebSocket server again + * after a failure + */ + export const WEBSOCKET_ATTEMPTS_WAIT = 1000; + /** WebSocket message type that indicates how to handle it */ + export enum MessageType { + InitClient = 'init-client', + ClientConnect = 'client-connect', + Request = 'request', + Response = 'response', + Event = 'event', + } + /** Message sent to the client to give it NetworkConnectorInfo */ + export type InitClient = { + type: MessageType.InitClient; + senderId: number; + connectorInfo: NetworkConnectorInfo; + /** Guid unique to this connection. Used to verify important messages like reconnecting */ + clientGuid: string; + }; + /** Message responding to the server to let it know this connection is ready to receive messages */ + export type ClientConnect = { + type: MessageType.ClientConnect; + senderId: number; + /** + * ClientGuid for this client the last time it was connected to the server. Used when reconnecting + * (like if the browser refreshes): if the server has a connection with this clientGuid, it will + * unregister all requests on that client so the reconnecting client can register its request + * handlers again. + */ + reconnectingClientGuid?: string; + }; + /** Request to do something and to respond */ + export type WebSocketRequest = { + type: MessageType.Request; + /** What kind of request this is. Certain command, etc */ + requestType: string; + } & InternalRequest; + /** Response to a request */ + export type WebSocketResponse = { + type: MessageType.Response; + /** What kind of request this is. Certain command, etc */ + requestType: string; + } & InternalResponse; + /** Event to be sent out throughout all processes */ + export type WebSocketEvent = { + type: MessageType.Event; + /** What kind of event this is */ + eventType: string; + } & InternalEvent; + /** Messages send by the WebSocket */ + export type Message = + | InitClient + | ClientConnect + | WebSocketRequest + | WebSocketResponse + | WebSocketEvent; +} +declare module 'shared/services/logger.service' { + import log from 'electron-log'; + export const WARN_TAG = ''; + /** + * Format a string of a service message + * + * @param message Message from the service + * @param serviceName Name of the service to show in the log + * @param tag Optional tag at the end of the service name + * @returns Formatted string of a service message + */ + export function formatLog(message: string, serviceName: string, tag?: string): string; + /** + * + * All extensions and services should use this logger to provide a unified output of logs + */ + const logger: log.MainLogger & { + default: log.MainLogger; + }; + export default logger; +} +declare module 'client/services/web-socket.interface' { + /** + * Interface that defines the webSocket functionality the extension host and the renderer must + * implement. Used by WebSocketFactory to supply the right kind of WebSocket to + * ClientNetworkConnector. For now, we are just using the browser WebSocket type. We may need + * specific functionality that don't line up between the ws library's implementation and the browser + * implementation. We can adjust as needed at that point. + */ + export type IWebSocket = WebSocket; +} +declare module 'renderer/services/renderer-web-socket.service' { + /** Once our network is running, run this to stop extensions from connecting to it directly */ + export const blockWebSocketsToPapiNetwork: () => void; + /** This wraps the browser's WebSocket implementation to provide + * better control over internet access. It is isomorphic with the standard WebSocket, so it should + * act as a drop-in replacement. + * + * Note that the Node WebSocket implementation is different and not wrapped here. + */ + export default class PapiRendererWebSocket implements WebSocket { + readonly CONNECTING: 0; + readonly OPEN: 1; + readonly CLOSING: 2; + readonly CLOSED: 3; + addEventListener: ( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ) => void; + binaryType: BinaryType; + bufferedAmount: number; + close: (code?: number, reason?: string) => void; + dispatchEvent: (event: Event) => boolean; + extensions: string; + onclose: ((this: WebSocket, ev: CloseEvent) => any) | null; + onerror: ((this: WebSocket, ev: Event) => any) | null; + onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null; + onopen: ((this: WebSocket, ev: Event) => any) | null; + protocol: string; + readyState: number; + removeEventListener: ( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ) => void; + send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void; + url: string; + constructor(url: string | URL, protocols?: string | string[]); + } +} +declare module 'extension-host/services/extension-host-web-socket.model' { + import ws from 'ws'; + /** + * Extension-host client uses ws as its WebSocket client, but the renderer can't use it. So we need + * to exclude it from the renderer webpack bundle like this. + */ + export default ws; +} +declare module 'client/services/web-socket.factory' { + import { IWebSocket } from 'client/services/web-socket.interface'; + /** + * Creates a WebSocket for the renderer or extension host depending on where you're running + * + * @returns WebSocket + */ + export const createWebSocket: (url: string) => Promise; +} +declare module 'client/services/client-network-connector.service' { + import { + ConnectionStatus, + InternalEvent, + InternalNetworkEventHandler, + InternalRequest, + InternalRequestHandler, + InternalResponse, + NetworkConnectorInfo, + RequestRouter, + } from 'shared/data/internal-connection.model'; + import INetworkConnector from 'shared/services/network-connector.interface'; + /** Handles the connection from the client to the server */ + export default class ClientNetworkConnector implements INetworkConnector { + connectorInfo: NetworkConnectorInfo; + connectionStatus: ConnectionStatus; + /** The webSocket connected to the server */ + private webSocket?; + /** + * All message subscriptions - emitters that emit an event each time a message with a specific + * message type comes in + */ + private messageEmitters; + /** + * Promise that resolves when the connection is finished or rejects if disconnected before the + * connection finishes + */ + private connectPromise?; + /** Function that removes this initClient handler from the connection */ + private unsubscribeHandleInitClientMessage?; + /** Function that removes this response handler from the connection */ + private unsubscribeHandleResponseMessage?; + /** Function that removes this handleRequest from the connection */ + private unsubscribeHandleRequestMessage?; + /** Function that removes this handleEvent from the connection */ + private unsubscribeHandleEventMessage?; + /** + * Function to call when we receive a request that is registered on this connector. Handles + * requests from the connection and returns a response to send back + */ + private localRequestHandler?; + /** + * Function to call when we are sending a request. Returns a clientId to which to send the request + * based on the requestType + */ + private requestRouter?; + /** + * Function to call when we receive an event. Handles events from the connection by emitting the + * event locally + */ + private localEventHandler?; + /** All requests that are waiting for a response */ + private requests; + /** Unique Guid associated with this connection. Used to verify certain things with server */ + private clientGuid; + connect: ( + localRequestHandler: InternalRequestHandler, + requestRouter: RequestRouter, + localEventHandler: InternalNetworkEventHandler, + ) => Promise< + Readonly<{ + clientId: number; + }> + >; + notifyClientConnected: () => Promise; + disconnect: () => void; + request: ( + requestType: string, + request: InternalRequest, + ) => Promise>; + emitEventOnNetwork: (eventType: string, event: InternalEvent) => Promise; /** - * Register a local request handler to run on requests. + * Send a message to the server via webSocket. Throws if not connected * - * @param requestType The type of request on which to register the handler - * @param handler Function to register to run on requests - * @param handlerType Type of handler function - indicates what type of parameters and what return - * type the handler has - * @returns Promise that resolves if the request successfully registered and unsubscriber function - * to run to stop the passed-in function from handling requests + * @param message Message to send */ - export function registerRequestHandler(requestType: SerializedRequestType, handler: ArgsRequestHandler, handlerType?: RequestHandlerType): Promise; - export function registerRequestHandler(requestType: SerializedRequestType, handler: ContentsRequestHandler, handlerType?: RequestHandlerType): Promise; - export function registerRequestHandler(requestType: SerializedRequestType, handler: ComplexRequestHandler, handlerType?: RequestHandlerType): Promise; + private sendMessage; /** - * Creates an event emitter that works properly over the network. Other connections receive this - * event when it is emitted. + * Receives and appropriately publishes server webSocket messages * - * WARNING: You can only create a network event emitter once per eventType to prevent hijacked event - * emitters. - * - * WARNING: You cannot emit events with complex types on the network. - * - * @param eventType Unique network event type for coordinating between connections - * @returns Event emitter whose event works between connections + * @param event WebSocket message information + * @param fromSelf Whether this message is from this connector instead of from someone else */ - export const createNetworkEventEmitter: (eventType: string) => PlatformEventEmitter; + private onMessage; /** - * Gets the network event with the specified type. Creates the emitter if it does not exist + * Subscribes a function to run on webSocket messages of a particular type * - * @param eventType Unique network event type for coordinating between connections - * @returns Event for the event type that runs the callback provided when the event is emitted + * @param messageType The type of message on which to subscribe the function + * @param callback Function to run with the contents of the webSocket message + * @returns Unsubscriber function to run to stop calling the passed-in function on webSocket + * messages */ - export const getNetworkEvent: (eventType: string) => PlatformEvent; + private subscribe; /** - * Creates a function that is a request function with a baked requestType. This is also nice because - * you get TypeScript type support using this function. + * Function that handles webSocket messages of type Response. Resolves the request associated with + * the received response message * - * @param requestType RequestType for request function - * @returns Function to call with arguments of request that performs the request and resolves with - * the response contents + * @param response Response message to resolve */ - export const createRequestFunction: (requestType: SerializedRequestType) => (...args: TParam) => Promise; - export interface PapiNetworkService { - onDidClientConnect: typeof onDidClientConnect; - onDidClientDisconnect: typeof onDidClientDisconnect; - createNetworkEventEmitter: typeof createNetworkEventEmitter; - getNetworkEvent: typeof getNetworkEvent; - } + private handleResponseMessage; /** - * JSDOC SOURCE papiNetworkService + * Function that handles incoming webSocket messages and locally sent messages of type Request. + * Runs the requestHandler provided in connect() and sends a message with the response * - * Service that provides a way to send and receive network events + * @param requestMessage Request message to handle + * @param isIncoming Whether this message is coming from the server and we should definitely + * handle it locally or if it is a locally sent request and we should send to the server if we + * don't have a local handler */ - export const papiNetworkService: PapiNetworkService; -} -declare module "shared/services/network-object.service" { - import { PlatformEvent, UnsubscriberAsync } from 'platform-bible-utils'; - import { NetworkObject, DisposableNetworkObject, NetworkableObject, LocalObjectToProxyCreator, NetworkObjectDetails } from "shared/models/network-object.model"; - /** Sets up the service. Only runs once and always returns the same promise after that */ - const initialize: () => Promise; + private handleRequestMessage; /** - * Search locally known network objects for the given ID. Don't look on the network for more - * objects. + * Function that handles incoming webSocket messages of type Event. Runs the eventHandler provided + * in connect() * - * @returns Whether we know of an existing network object with the provided ID already on the - * network - */ - const hasKnown: (id: string) => boolean; - /** - * Event that fires when a new object has been created on the network (locally or remotely). The - * event contains information about the new network object. + * @param eventMessage Event message to handle */ - export const onDidCreateNetworkObject: PlatformEvent; - /** Event that fires with a network object ID when that object is disposed locally or remotely */ - export const onDidDisposeNetworkObject: PlatformEvent; - interface IDisposableObject { - dispose?: UnsubscriberAsync; - } - /** If `dispose` already exists on `objectToMutate`, we will call it in addition to `newDispose` */ - export function overrideDispose(objectToMutate: IDisposableObject, newDispose: UnsubscriberAsync): void; - /** - * Get a network object that has previously been set up to be shared on the network. A network - * object is a proxy to an object living somewhere else that local code can use. - * - * Running this function twice with the same inputs yields the same network object. - * - * @param id ID of the network object - all processes must use this ID to look up this network - * object - * @param createLocalObjectToProxy Function that creates an object that the network object proxy - * will be based upon. The object this function creates cannot have an `onDidDispose` property. - * This function is useful for setting up network events on a network object. - * @returns A promise for the network object with specified ID if one exists, undefined otherwise - */ - const get: (id: string, createLocalObjectToProxy?: LocalObjectToProxyCreator | undefined) => Promise | undefined>; - /** - * Set up an object to be shared on the network. - * - * @param id ID of the object to share on the network. All processes must use this ID to look it up. - * @param objectToShare The object to set up as a network object. It will have an event named - * `onDidDispose` added to its properties. An error will be thrown if the object already had an - * `onDidDispose` property on it. If the object already contained a `dispose` function, a new - * `dispose` function will be set that calls the existing function (amongst other things). If the - * object did not already define a `dispose` function, one will be added. - * - * WARNING: setting a network object mutates the provided object. - * @returns `objectToShare` modified to be a network object - */ - const set: (id: string, objectToShare: T, objectType?: string, objectAttributes?: { - [property: string]: unknown; - } | undefined) => Promise>; - interface NetworkObjectService { - initialize: typeof initialize; - hasKnown: typeof hasKnown; - get: typeof get; - set: typeof set; - onDidCreateNetworkObject: typeof onDidCreateNetworkObject; - } - /** - * Network objects are distributed objects within PAPI for TS/JS objects. @see - * https://en.wikipedia.org/wiki/Distributed_object - * - * Objects registered via {@link networkObjectService.set} are retrievable using - * {@link networkObjectService.get}. - * - * Function calls made on network objects retrieved via {@link networkObjectService.get} are proxied - * and sent to the original objects registered via {@link networkObjectService.set}. All functions on - * the registered object are proxied except for constructors, `dispose`, and functions starting with - * `on` since those should be events (which are not intended to be proxied) based on our naming - * convention. If you don't want a function to be proxied, don't make it a property of the - * registered object. - * - * Functions on a network object will be called asynchronously by other processes regardless of - * whether the functions are synchronous or asynchronous, so it is best to make them all - * asynchronous. All shared functions' arguments and return values must be serializable to be called - * across processes. - * - * When a service registers an object via {@link networkObjectService.set}, it is the responsibility - * of that service, and only that service, to call `dispose` on that object when it is no longer - * intended to be shared with other services. - * - * When an object is disposed by calling `dispose`, all functions registered with the `onDidDispose` - * event handler will be called. After an object is disposed, calls to its functions will no longer - * be proxied to the original object. - */ - const networkObjectService: NetworkObjectService; - export default networkObjectService; -} -declare module "shared/models/network-object.model" { - import { Dispose, OnDidDispose, CannotHaveOnDidDispose, CanHaveOnDidDispose } from 'platform-bible-utils'; - /** - * An object of this type is returned from {@link networkObjectService.get}. - * - * Override the NetworkableObject type's force-undefined onDidDispose to NetworkObject's - * onDidDispose type because it will have an onDidDispose added. - * - * If an object of type T had `dispose` on it, `networkObjectService.get` will remove the ability to - * 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 - */ - export type NetworkObject = Omit, 'dispose'> & OnDidDispose; - /** - * An object of this type is returned from {@link networkObjectService.set}. - * - * @see networkObjectService - */ - export type DisposableNetworkObject = NetworkObject & Dispose; - /** - * An object of this type is passed into {@link networkObjectService.set}. - * - * @see networkObjectService - */ - export type NetworkableObject = T & CannotHaveOnDidDispose; - /** - * If a network object with the provided ID exists remotely but has not been set up to use inside - * this process, this function is run in {@link networkObjectService.get}, and the returned object is - * used as a base on which to set up a NetworkObject for use on this process. All properties that - * are exposed in the base object will be used as-is, and all other properties will be assumed to - * exist on the remote network object. - * - * @param id ID of the network object to get - * @param networkObjectContainer Holds a reference to the NetworkObject that will be setup within - * {@link networkObjectService.get}. It is passed in to allow the return value to call functions on - * the NetworkObject. NOTE: networkObjectContainer.contents does not point to a real NetworkObject - * while this function is running. The real reference is assigned later, but before the - * NetworkObject will be used. The return value should always reference the NetworkObject as - * `networkObjectContainer.contents` to avoid acting upon an undefined NetworkObject. - * @returns The local object to proxy into a network object. - * - * 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 - */ - export type LocalObjectToProxyCreator = (id: string, networkObjectPromise: Promise>) => Partial; - /** Data about an object shared on the network */ - export type NetworkObjectDetails = { - /** ID of the network object that processes use to reference it */ - id: string; - /** - * Name of the type of this network object. Note this isn't about TypeScript types, but instead - * focused on the platform data model. Names of types for the same logical thing (e.g., Project - * Data Providers => `pdp`) should be the same across all process on the network regardless of - * what programming language they use. For generic network objects, `networkObject` is - * appropriate. - */ - objectType: string; - /** Array of strings with the function names exposed on this network object */ - functionNames: string[]; - /** - * Optional object containing properties that describe this network object. The properties - * associated with this network object depend on the `objectType`. - */ - attributes?: Record; - }; + private handleEventMessage; + } } -declare module "shared/models/data-provider.model" { - import { UnsubscriberAsync, PlatformEventHandler } from 'platform-bible-utils'; - import { NetworkableObject } from "shared/models/network-object.model"; - /** Various options to adjust how the data provider subscriber emits updates */ - export type DataProviderSubscriberOptions = { - /** - * Whether to immediately retrieve the data for this subscriber and run the callback as soon as - * possible. - * - * This allows a subscriber to simply subscribe and provide a callback instead of subscribing, - * running `get`, and managing the race condition of an event coming in to update the data and the - * initial `get` coming back in. - * - * @default true - */ - retrieveDataImmediately?: boolean; - /** - * Under which conditions to run the callback when we receive updates to the data. - * - * - `'deeply-equal'` - only run the update callback when the data at this selector has changed. - * - * For example, suppose your selector is targeting John 3:5, and the data provider updates its - * data for Luke 5:3. Your data at John 3:5 does not change, and your callback will not run. - * - `'*'` - run the update callback every time the data has been updated whether or not the data at - * this selector has changed. - * - * For example, suppose your selector is targeting John 3:5, and the data provider updates its - * data for Luke 5:3. Your data at John 3:5 does not change, but your callback will run again - * with the same data anyway. - * - * @default 'deeply-equal' - */ - whichUpdates?: 'deeply-equal' | '*'; - }; +declare module 'main/services/server-network-connector.service' { + import { + ConnectionStatus, + InternalEvent, + InternalNetworkEventHandler, + InternalRequest, + InternalRequestHandler, + InternalResponse, + NetworkConnectorEventHandlers, + NetworkConnectorInfo, + RequestRouter, + } from 'shared/data/internal-connection.model'; + import INetworkConnector from 'shared/services/network-connector.interface'; + /** Handles the endpoint and connections from the server to the clients */ + export default class ServerNetworkConnector implements INetworkConnector { + connectorInfo: NetworkConnectorInfo; + connectionStatus: ConnectionStatus; + /** The webSocket connected to the server */ + private webSocketServer?; + /** The next client id to use for a new connection. Starts at 1 because the server is 0 */ + private nextClientId; + /** The webSocket clients that are connected and information about them */ + private clientSockets; + /** + * All message subscriptions - emitters that emit an event each time a message with a specific + * message type comes in + */ + private messageEmitters; + /** + * Promise that resolves when finished starting the server or rejects if disconnected before the + * server finishes + */ + private connectPromise?; + /** Function that removes this clientConnect handler from connections */ + private unsubscribeHandleClientConnectMessage?; + /** Function that removes this response handler from connections */ + private unsubscribeHandleResponseMessage?; + /** Function that removes this handleRequest from connections */ + private unsubscribeHandleRequestMessage?; + /** Function that removes this handleEvent from the connection */ + private unsubscribeHandleEventMessage?; + /** + * Function to call when we receive a request that is registered on this connector. Handles + * requests from connections and returns a response to send back + */ + private localRequestHandler?; + /** + * Function to call when we are sending a request. Returns a clientId to which to send the request + * based on the requestType + */ + private requestRouter?; + /** + * Function to call when we receive an event. Handles events from connections and emits the event + * locally + */ + private localEventHandler?; + /** Functions to run when network connector events occur like when clients are disconnected */ + private networkConnectorEventHandlers?; + /** All requests that are waiting for a response */ + private requests; + connect: ( + localRequestHandler: InternalRequestHandler, + requestRouter: RequestRouter, + localEventHandler: InternalNetworkEventHandler, + networkConnectorEventHandlers: NetworkConnectorEventHandlers, + ) => Promise< + Readonly<{ + clientId: number; + }> + >; + notifyClientConnected: () => Promise; + disconnect: () => void; + request: ( + requestType: string, + request: InternalRequest, + ) => Promise>; + emitEventOnNetwork: (eventType: string, event: InternalEvent) => Promise; + /** Get the client socket for a certain clientId. Throws if not found */ + private getClientSocket; /** - * Information that papi uses to interpret whether to send out updates on a data provider when the - * engine runs `set` or `notifyUpdate`. - * - * - `'*'` update subscriptions for all data types on this data provider - * - `string` name of data type - update subscriptions for this data type - * - `string[]` names of data types - update subscriptions for the data types in the array - * - `true` (or other truthy values other than strings and arrays) - * - * - In `set` - update subscriptions for this data type - * - In `notifyUpdate` - same as '*' - * - `false` (or falsy) do not update subscriptions + * Attempts to get the client socket for a certain clientGuid. Returns undefined if not found. + * This does not throw because it will likely be very common that we do not have a clientId for a + * certain clientGuid as connecting clients will often supply old clientGuids. */ - export type DataProviderUpdateInstructions = '*' | DataTypeNames | DataTypeNames[] | boolean; + private getClientSocketFromGuid; + /** Get the clientId for a certain webSocket. Throws if not found */ + private getClientIdFromSocket; /** - * Set a subset of data according to the selector. - * - * Note: if a data provider engine does not provide `set` (possibly indicating it is read-only), - * this will throw an exception. + * Send a message to a client via webSocket. Throws if not connected * - * @param selector Tells the provider what subset of data is being set - * @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 + * @param message Message to send + * @param recipientId The client to which to send the message. TODO: determine if we can intuit + * this instead */ - export type DataProviderSetter = (selector: TDataTypes[DataType]['selector'], data: TDataTypes[DataType]['setData']) => Promise>; + private sendMessage; /** - * Get a subset of data from the provider according to the selector. + * Receives and appropriately publishes webSocket messages * - * Note: This is good for retrieving data from a provider once. If you want to keep the data - * up-to-date, use `subscribe` instead, which can immediately give you the data and keep it - * up-to-date. - * - * @param selector Tells the provider what subset of data to get - * @returns The subset of data represented by the selector + * @param event WebSocket message information + * @param fromSelf Whether this message is from this connector instead of from someone else */ - export type DataProviderGetter = (selector: TDataType['selector']) => Promise; - /** - * Subscribe to receive updates relevant to the provided selector from this data provider for a - * specific data type. - * - * Note: By default, this `subscribe` function automatically retrieves the current state - * of the data 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 `get`. You can turn this - * functionality off in the `options` parameter. - * - * @param selector Tells the provider what data this listener is listening for - * @param callback Function to run with the updated data for this selector - * @param options Various options to adjust how the subscriber emits updates - * @returns Unsubscriber to stop listening for updates - */ - export type DataProviderSubscriber = (selector: TDataType['selector'], callback: PlatformEventHandler, options?: DataProviderSubscriberOptions) => Promise; - /** - * A helper type describing the types associated with a data provider's methods for a specific data - * type it handles. - * - * @type `TSelector` - The type of selector used to get some data from this provider at this data - * type. A selector is an object a caller provides to the data provider to tell the provider what - * subset of data it wants at this data type. - * @type `TGetData` - The type of data provided by this data provider when you run `get` - * based on a provided selector - * @type `TSetData` - The type of data ingested by this data provider when you run `set` - * based on a provided selector - */ - export type DataProviderDataType = { - /** - * The type of selector used to get some data from this provider at this data type. A selector is - * an object a caller provides to the data provider to tell the provider what subset of data it - * wants at this data type. - */ - selector: TSelector; - /** - * The type of data provided by this data provider when you run `get` based on a - * provided selector - */ - getData: TGetData; - /** - * The type of data ingested by this data provider when you run `set` based on a - * provided selector - */ - setData: TSetData; - }; + private onMessage; /** - * A helper type describing all the data types a data provider handles. Each property on this type - * (consisting of a DataProviderDataType, which describes the types that correspond to that data - * type) describes a data type that the data provider handles. The data provider has a - * `set`, `get`, and `subscribe` for each property (aka data type) - * listed in this type. + * Subscribes a function to run on webSocket messages of a particular type * - * @example A data provider that handles greeting strings and age numbers (as well as an All data - * type that just provides all the data) could have a DataProviderDataTypes that looks like the - * following: - * - * ```typescript - * { - * Greeting: DataProviderDataType; - * Age: DataProviderDataType; - * All: DataProviderDataType; - * } - * ``` + * @param messageType The type of message on which to subscribe the function + * @param callback Function to run with the contents of the webSocket message + * @returns Unsubscriber function to run to stop calling the passed-in function on webSocket + * messages */ - export type DataProviderDataTypes = { - [dataType: string]: DataProviderDataType; - }; + private subscribe; /** - * 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 + * Registers an incoming webSocket connection and sends connection info with InitClient. Does not + * consider the client fully connected yet until they respond and tell us they connected with + * ClientConnect */ - export type DataTypeNames = keyof TDataTypes & string; + private onClientConnect; + /** Handles when client connection disconnects. Unregisters and such */ + private onClientDisconnect; + /** Closes connection and unregisters a client webSocket when it has disconnected */ + private disconnectClient; /** - * Set of all `set` methods that a data provider provides according to its data types. + * Function that handles webSocket messages of type ClientConnect. Mark the connection fully + * connected and notify that a client connected or reconnected * - * @see DataProviderSetter for more information + * @param clientConnect Message from the client about the connection + * @param connectorId ClientId of the client who is sending this ClientConnect message */ - export type DataProviderSetters = { - [DataType in keyof TDataTypes as `set${DataType & string}`]: DataProviderSetter; - }; + private handleClientConnectMessage; /** - * Set of all `get` methods that a data provider provides according to its data types. + * Function that handles webSocket messages of type Response. Resolves the request associated with + * the received response message or forwards to appropriate client * - * @see DataProviderGetter for more information + * @param response Response message to resolve + * @param responderId Responding client */ - export type DataProviderGetters = { - [DataType in keyof TDataTypes as `get${DataType & string}`]: DataProviderGetter; - }; + private handleResponseMessage; /** - * Set of all `subscribe` methods that a data provider provides according to its data - * types. + * Function that handles incoming webSocket messages and locally sent messages of type Request. + * Handles the request and sends a response if we have a handler or forwards to the appropriate + * client * - * @see DataProviderSubscriber for more information + * @param requestMessage Request to handle + * @param requesterId Who sent this message */ - export type DataProviderSubscribers = { - [DataType in keyof TDataTypes as `subscribe${DataType & string}`]: DataProviderSubscriber; - }; - /** - * An internal object created locally when someone runs dataProviderService.registerEngine. This - * 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 - */ - type DataProviderInternal = NetworkableObject & DataProviderGetters & DataProviderSubscribers>; - /** - * Get the data type for a data provider function based on its name - * - * @param fnName Name of data provider function e.g. `getVerse` - * @returns Data type for that data provider function e.g. `Verse` - */ - export function getDataProviderDataTypeFromFunctionName(fnName: string): DataTypeNames; - export default DataProviderInternal; -} -declare module "shared/models/project-data-provider.model" { - import type { DataProviderDataType } 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 - * 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 PDP for accessing extension data. There - * is no way to get or set just a portion of data identified by a single dataQualifier value. - */ - dataQualifier: string; - }; + private handleRequestMessage; /** - * 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: + * Function that handles incoming webSocket messages of type Event. Runs the eventHandler provided + * in connect() and forwards the event to other clients * - * - All PSIs 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 - * - In the future, we may enforce that callers to `ExtensionData` endpoints include `extensionName`, - * so following this interface ensures your PDP will not break if such a requirement is - * implemented. + * @param eventMessage Event message to handle */ - export type MandatoryProjectDataType = { - ExtensionData: DataProviderDataType; - }; + private handleEventMessage; + } } -declare module "shared/models/data-provider.interface" { - import { DataProviderDataTypes, DataProviderGetters, DataProviderSetters, DataProviderSubscribers } from "shared/models/data-provider.model"; - 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. - * - * Note: each `set` method has a corresponding `get` and - * `subscribe` method. - */ - type IDataProvider = DataProviderSetters & DataProviderGetters & DataProviderSubscribers & OnDidDispose; - export default IDataProvider; - /** - * A data provider that has control over disposing of it with dispose. Returned from registering a - * data provider (only the service that set it up should dispose of it) with - * dataProviderService.registerEngine - * - * @see IDataProvider - */ - export type IDisposableDataProvider> = TDataProvider & Dispose; +declare module 'shared/services/network-connector.factory' { + import INetworkConnector from 'shared/services/network-connector.interface'; + /** + * Creates a NetworkConnector for the client or the server depending on where you're running + * + * @returns NetworkConnector + */ + export const createNetworkConnector: () => Promise; +} +declare module 'shared/services/connection.service' { + /** + * Handles setting up a connection to the electron backend and exchanging simple messages. Do not + * use outside NetworkService.ts. For communication, use NetworkService.ts as it is an abstraction + * over this. + */ + import { + NetworkConnectorEventHandlers, + NetworkEventHandler, + RequestHandler, + RequestRouter, + } from 'shared/data/internal-connection.model'; + import { ComplexResponse } from 'shared/utils/util'; + /** + * Send a request to the server and resolve after receiving a response + * + * @param requestType The type of request + * @param contents Contents to send in the request + * @returns Promise that resolves with the response message + */ + export const request: ( + requestType: string, + contents: TParam, + ) => Promise>; + /** + * Sends an event to other processes. Does NOT run the local event subscriptions as they should be + * run by NetworkEventEmitter after sending on network. + * + * @param eventType Unique network event type for coordinating between processes + * @param event Event to emit on the network + */ + export const emitEventOnNetwork: (eventType: string, event: T) => Promise; + /** Disconnects from the server */ + export const disconnect: () => void; + /** + * Sets up the ConnectionService by connecting to the server and setting up event handlers + * + * @param localRequestHandler Function that handles requests from the server by accepting a + * requestType and a ComplexRequest and returning a Promise of a Complex Response + * @param networkRequestRouter Function that determines the appropriate clientId to which to send + * requests of the given type + * @param localEventHandler Function that handles events from the server by accepting an eventType + * and an event and emitting the event locally + * @param connectorEventHandlers Functions that run when network connector events occur like when + * clients are disconnected + * @returns Promise that resolves when finished connecting + */ + export const connect: ( + localRequestHandler: RequestHandler, + networkRequestRouter: RequestRouter, + localEventHandler: NetworkEventHandler, + connectorEventHandlers: NetworkConnectorEventHandlers, + ) => Promise; + /** Gets this connection's clientId */ + export const getClientId: () => number; +} +declare module 'shared/models/papi-network-event-emitter.model' { + import { PlatformEventHandler, PlatformEventEmitter } from 'platform-bible-utils'; + /** + * Networked version of EventEmitter - accepts subscriptions to an event and runs the subscription + * callbacks when the event is emitted. Events on NetworkEventEmitters can be emitted across + * processes. They are coordinated between processes by their type. Use eventEmitter.event(callback) + * to subscribe to the event. Use eventEmitter.emit(event) to run the subscriptions. Generally, this + * EventEmitter should be private, and its event should be public. That way, the emitter is not + * publicized, but anyone can subscribe to the event. + * + * WARNING: Do not use this class directly outside of NetworkService, or it will not do what you + * expect. Use NetworkService.createNetworkEventEmitter. + * + * WARNING: You cannot emit events with complex types on the network. + */ + export default class PapiNetworkEventEmitter extends PlatformEventEmitter { + /** Callback that sends the event to other processes on the network when it is emitted */ + private networkSubscriber; + /** Callback that runs when the emitter is disposed - should handle unlinking from the network */ + private networkDisposer; + /** + * Creates a NetworkEventEmitter + * + * @param networkSubscriber Callback that accepts the event and emits it to other processes + * @param networkDisposer Callback that unlinks this emitter from the network + */ + constructor( + /** Callback that sends the event to other processes on the network when it is emitted */ + networkSubscriber: PlatformEventHandler, + /** Callback that runs when the emitter is disposed - should handle unlinking from the network */ + networkDisposer: () => void, + ); + emit: (event: T) => void; + /** + * Runs only the subscriptions for the event that are on this process. Does not send over network + * + * @param event Event data to provide to subscribed callbacks + */ + emitLocal(event: T): void; + dispose: () => Promise; + } +} +declare module 'shared/services/network.service' { + /** + * Handles requests, responses, subscriptions, etc. to the backend. Likely shouldn't need/want to + * expose this whole service on papi, but there are a few things that are exposed via + * papiNetworkService + */ + import { ClientConnectEvent, ClientDisconnectEvent } from 'shared/data/internal-connection.model'; + import { UnsubscriberAsync, PlatformEventEmitter, PlatformEvent } from 'platform-bible-utils'; + import { + ComplexRequest, + ComplexResponse, + RequestHandlerType, + SerializedRequestType, + } from 'shared/utils/util'; + /** + * Args handler function for a request. Called when a request is handled. The function should accept + * the spread of the contents array of the request as its parameters. The function should return an + * object that becomes the contents object of the response. This type of handler is a normal + * function. + */ + type ArgsRequestHandler = any[], TReturn = any> = ( + ...args: TParam + ) => Promise | TReturn; + /** + * Contents handler function for a request. Called when a request is handled. The function should + * accept the contents object of the request as its single parameter. The function should return an + * object that becomes the contents object of the response. + */ + type ContentsRequestHandler = (contents: TParam) => Promise; + /** + * Complex handler function for a request. Called when a request is handled. The function should + * accept a ComplexRequest object as its single parameter. The function should return a + * ComplexResponse object that becomes the response.. This type of handler is the most flexible of + * the request handlers. + */ + type ComplexRequestHandler = ( + request: ComplexRequest, + ) => Promise>; + /** Event that emits with clientId when a client connects */ + export const onDidClientConnect: PlatformEvent; + /** Event that emits with clientId when a client disconnects */ + export const onDidClientDisconnect: PlatformEvent; + /** Closes the network services gracefully */ + export const shutdown: () => void; + /** Sets up the NetworkService. Runs only once */ + export const initialize: () => Promise; + /** + * Send a request on the network and resolve the response contents. + * + * @param requestType The type of request + * @param args Arguments to send in the request (put in request.contents) + * @returns Promise that resolves with the response message + */ + export const request: ( + requestType: SerializedRequestType, + ...args: TParam + ) => Promise; + /** + * Register a local request handler to run on requests. + * + * @param requestType The type of request on which to register the handler + * @param handler Function to register to run on requests + * @param handlerType Type of handler function - indicates what type of parameters and what return + * type the handler has + * @returns Promise that resolves if the request successfully registered and unsubscriber function + * to run to stop the passed-in function from handling requests + */ + export function registerRequestHandler( + requestType: SerializedRequestType, + handler: ArgsRequestHandler, + handlerType?: RequestHandlerType, + ): Promise; + export function registerRequestHandler( + requestType: SerializedRequestType, + handler: ContentsRequestHandler, + handlerType?: RequestHandlerType, + ): Promise; + export function registerRequestHandler( + requestType: SerializedRequestType, + handler: ComplexRequestHandler, + handlerType?: RequestHandlerType, + ): Promise; + /** + * Creates an event emitter that works properly over the network. Other connections receive this + * event when it is emitted. + * + * WARNING: You can only create a network event emitter once per eventType to prevent hijacked event + * emitters. + * + * WARNING: You cannot emit events with complex types on the network. + * + * @param eventType Unique network event type for coordinating between connections + * @returns Event emitter whose event works between connections + */ + export const createNetworkEventEmitter: (eventType: string) => PlatformEventEmitter; + /** + * Gets the network event with the specified type. Creates the emitter if it does not exist + * + * @param eventType Unique network event type for coordinating between connections + * @returns Event for the event type that runs the callback provided when the event is emitted + */ + export const getNetworkEvent: (eventType: string) => PlatformEvent; + /** + * Creates a function that is a request function with a baked requestType. This is also nice because + * you get TypeScript type support using this function. + * + * @param requestType RequestType for request function + * @returns Function to call with arguments of request that performs the request and resolves with + * the response contents + */ + export const createRequestFunction: ( + requestType: SerializedRequestType, + ) => (...args: TParam) => Promise; + export interface PapiNetworkService { + onDidClientConnect: typeof onDidClientConnect; + onDidClientDisconnect: typeof onDidClientDisconnect; + createNetworkEventEmitter: typeof createNetworkEventEmitter; + getNetworkEvent: typeof getNetworkEvent; + } + /** + * + * Service that provides a way to send and receive network events + */ + export const papiNetworkService: PapiNetworkService; +} +declare module 'shared/services/network-object.service' { + import { PlatformEvent, UnsubscriberAsync } from 'platform-bible-utils'; + import { + NetworkObject, + DisposableNetworkObject, + NetworkableObject, + LocalObjectToProxyCreator, + NetworkObjectDetails, + } from 'shared/models/network-object.model'; + /** Sets up the service. Only runs once and always returns the same promise after that */ + const initialize: () => Promise; + /** + * Search locally known network objects for the given ID. Don't look on the network for more + * objects. + * + * @returns Whether we know of an existing network object with the provided ID already on the + * network + */ + const hasKnown: (id: string) => boolean; + /** + * Event that fires when a new object has been created on the network (locally or remotely). The + * event contains information about the new network object. + */ + export const onDidCreateNetworkObject: PlatformEvent; + /** Event that fires with a network object ID when that object is disposed locally or remotely */ + export const onDidDisposeNetworkObject: PlatformEvent; + interface IDisposableObject { + dispose?: UnsubscriberAsync; + } + /** If `dispose` already exists on `objectToMutate`, we will call it in addition to `newDispose` */ + export function overrideDispose( + objectToMutate: IDisposableObject, + newDispose: UnsubscriberAsync, + ): void; + /** + * Get a network object that has previously been set up to be shared on the network. A network + * object is a proxy to an object living somewhere else that local code can use. + * + * Running this function twice with the same inputs yields the same network object. + * + * @param id ID of the network object - all processes must use this ID to look up this network + * object + * @param createLocalObjectToProxy Function that creates an object that the network object proxy + * will be based upon. The object this function creates cannot have an `onDidDispose` property. + * This function is useful for setting up network events on a network object. + * @returns A promise for the network object with specified ID if one exists, undefined otherwise + */ + const get: ( + id: string, + createLocalObjectToProxy?: LocalObjectToProxyCreator | undefined, + ) => Promise | undefined>; + /** + * Set up an object to be shared on the network. + * + * @param id ID of the object to share on the network. All processes must use this ID to look it up. + * @param objectToShare The object to set up as a network object. It will have an event named + * `onDidDispose` added to its properties. An error will be thrown if the object already had an + * `onDidDispose` property on it. If the object already contained a `dispose` function, a new + * `dispose` function will be set that calls the existing function (amongst other things). If the + * object did not already define a `dispose` function, one will be added. + * + * WARNING: setting a network object mutates the provided object. + * @returns `objectToShare` modified to be a network object + */ + const set: ( + id: string, + objectToShare: T, + objectType?: string, + objectAttributes?: + | { + [property: string]: unknown; + } + | undefined, + ) => Promise>; + interface NetworkObjectService { + initialize: typeof initialize; + hasKnown: typeof hasKnown; + get: typeof get; + set: typeof set; + onDidCreateNetworkObject: typeof onDidCreateNetworkObject; + } + /** + * Network objects are distributed objects within PAPI for TS/JS objects. @see + * https://en.wikipedia.org/wiki/Distributed_object + * + * Objects registered via {@link networkObjectService.set} are retrievable using + * {@link networkObjectService.get}. + * + * Function calls made on network objects retrieved via {@link networkObjectService.get} are proxied + * and sent to the original objects registered via {@link networkObjectService.set}. All functions on + * the registered object are proxied except for constructors, `dispose`, and functions starting with + * `on` since those should be events (which are not intended to be proxied) based on our naming + * convention. If you don't want a function to be proxied, don't make it a property of the + * registered object. + * + * Functions on a network object will be called asynchronously by other processes regardless of + * whether the functions are synchronous or asynchronous, so it is best to make them all + * asynchronous. All shared functions' arguments and return values must be serializable to be called + * across processes. + * + * When a service registers an object via {@link networkObjectService.set}, it is the responsibility + * of that service, and only that service, to call `dispose` on that object when it is no longer + * intended to be shared with other services. + * + * When an object is disposed by calling `dispose`, all functions registered with the `onDidDispose` + * event handler will be called. After an object is disposed, calls to its functions will no longer + * be proxied to the original object. + */ + const networkObjectService: NetworkObjectService; + export default networkObjectService; +} +declare module 'shared/models/network-object.model' { + import { + Dispose, + OnDidDispose, + CannotHaveOnDidDispose, + CanHaveOnDidDispose, + } from 'platform-bible-utils'; + /** + * An object of this type is returned from {@link networkObjectService.get}. + * + * Override the NetworkableObject type's force-undefined onDidDispose to NetworkObject's + * onDidDispose type because it will have an onDidDispose added. + * + * If an object of type T had `dispose` on it, `networkObjectService.get` will remove the ability to + * 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 + */ + export type NetworkObject = Omit, 'dispose'> & + OnDidDispose; + /** + * An object of this type is returned from {@link networkObjectService.set}. + * + * @see networkObjectService + */ + export type DisposableNetworkObject = NetworkObject & Dispose; + /** + * An object of this type is passed into {@link networkObjectService.set}. + * + * @see networkObjectService + */ + export type NetworkableObject = T & CannotHaveOnDidDispose; + /** + * If a network object with the provided ID exists remotely but has not been set up to use inside + * this process, this function is run in {@link networkObjectService.get}, and the returned object is + * used as a base on which to set up a NetworkObject for use on this process. All properties that + * are exposed in the base object will be used as-is, and all other properties will be assumed to + * exist on the remote network object. + * + * @param id ID of the network object to get + * @param networkObjectContainer Holds a reference to the NetworkObject that will be setup within + * {@link networkObjectService.get}. It is passed in to allow the return value to call functions on + * the NetworkObject. NOTE: networkObjectContainer.contents does not point to a real NetworkObject + * while this function is running. The real reference is assigned later, but before the + * NetworkObject will be used. The return value should always reference the NetworkObject as + * `networkObjectContainer.contents` to avoid acting upon an undefined NetworkObject. + * @returns The local object to proxy into a network object. + * + * 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 + */ + export type LocalObjectToProxyCreator = ( + id: string, + networkObjectPromise: Promise>, + ) => Partial; + /** Data about an object shared on the network */ + export type NetworkObjectDetails = { + /** ID of the network object that processes use to reference it */ + id: string; + /** + * Name of the type of this network object. Note this isn't about TypeScript types, but instead + * focused on the platform data model. Names of types for the same logical thing (e.g., Project + * Data Providers => `pdp`) should be the same across all process on the network regardless of + * what programming language they use. For generic network objects, `networkObject` is + * appropriate. + */ + objectType: string; + /** Array of strings with the function names exposed on this network object */ + functionNames: string[]; + /** + * Optional object containing properties that describe this network object. The properties + * associated with this network object depend on the `objectType`. + */ + attributes?: Record; + }; +} +declare module 'shared/models/data-provider.model' { + import { UnsubscriberAsync, PlatformEventHandler } from 'platform-bible-utils'; + import { NetworkableObject } from 'shared/models/network-object.model'; + /** Various options to adjust how the data provider subscriber emits updates */ + export type DataProviderSubscriberOptions = { + /** + * Whether to immediately retrieve the data for this subscriber and run the callback as soon as + * possible. + * + * This allows a subscriber to simply subscribe and provide a callback instead of subscribing, + * running `get`, and managing the race condition of an event coming in to update the data and the + * initial `get` coming back in. + * + * @default true + */ + retrieveDataImmediately?: boolean; + /** + * Under which conditions to run the callback when we receive updates to the data. + * + * - `'deeply-equal'` - only run the update callback when the data at this selector has changed. + * + * For example, suppose your selector is targeting John 3:5, and the data provider updates its + * data for Luke 5:3. Your data at John 3:5 does not change, and your callback will not run. + * - `'*'` - run the update callback every time the data has been updated whether or not the data at + * this selector has changed. + * + * For example, suppose your selector is targeting John 3:5, and the data provider updates its + * data for Luke 5:3. Your data at John 3:5 does not change, but your callback will run again + * with the same data anyway. + * + * @default 'deeply-equal' + */ + whichUpdates?: 'deeply-equal' | '*'; + }; + /** + * Information that papi uses to interpret whether to send out updates on a data provider when the + * engine runs `set` or `notifyUpdate`. + * + * - `'*'` update subscriptions for all data types on this data provider + * - `string` name of data type - update subscriptions for this data type + * - `string[]` names of data types - update subscriptions for the data types in the array + * - `true` (or other truthy values other than strings and arrays) + * + * - In `set` - update subscriptions for this data type + * - In `notifyUpdate` - same as '*' + * - `false` (or falsy) do not update subscriptions + */ + export type DataProviderUpdateInstructions = + | '*' + | DataTypeNames + | DataTypeNames[] + | boolean; + /** + * Set a subset of data according to the selector. + * + * Note: if a data provider engine does not provide `set` (possibly indicating it is read-only), + * this will throw an exception. + * + * @param selector Tells the provider what subset of data is being set + * @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 + */ + export type DataProviderSetter< + TDataTypes extends DataProviderDataTypes, + DataType extends keyof TDataTypes, + > = ( + selector: TDataTypes[DataType]['selector'], + data: TDataTypes[DataType]['setData'], + ) => Promise>; + /** + * Get a subset of data from the provider according to the selector. + * + * Note: This is good for retrieving data from a provider once. If you want to keep the data + * up-to-date, use `subscribe` instead, which can immediately give you the data and keep it + * up-to-date. + * + * @param selector Tells the provider what subset of data to get + * @returns The subset of data represented by the selector + */ + export type DataProviderGetter = ( + selector: TDataType['selector'], + ) => Promise; + /** + * Subscribe to receive updates relevant to the provided selector from this data provider for a + * specific data type. + * + * Note: By default, this `subscribe` function automatically retrieves the current state + * of the data 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 `get`. You can turn this + * functionality off in the `options` parameter. + * + * @param selector Tells the provider what data this listener is listening for + * @param callback Function to run with the updated data for this selector + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber to stop listening for updates + */ + export type DataProviderSubscriber = ( + selector: TDataType['selector'], + callback: PlatformEventHandler, + options?: DataProviderSubscriberOptions, + ) => Promise; + /** + * A helper type describing the types associated with a data provider's methods for a specific data + * type it handles. + * + * @type `TSelector` - The type of selector used to get some data from this provider at this data + * type. A selector is an object a caller provides to the data provider to tell the provider what + * subset of data it wants at this data type. + * @type `TGetData` - The type of data provided by this data provider when you run `get` + * based on a provided selector + * @type `TSetData` - The type of data ingested by this data provider when you run `set` + * based on a provided selector + */ + export type DataProviderDataType< + TSelector = unknown, + TGetData = TSelector, + TSetData = TGetData, + > = { + /** + * The type of selector used to get some data from this provider at this data type. A selector is + * an object a caller provides to the data provider to tell the provider what subset of data it + * wants at this data type. + */ + selector: TSelector; + /** + * The type of data provided by this data provider when you run `get` based on a + * provided selector + */ + getData: TGetData; + /** + * The type of data ingested by this data provider when you run `set` based on a + * provided selector + */ + setData: TSetData; + }; + /** + * A helper type describing all the data types a data provider handles. Each property on this type + * (consisting of a DataProviderDataType, which describes the types that correspond to that data + * type) describes a data type that the data provider handles. The data provider has a + * `set`, `get`, and `subscribe` for each property (aka data type) + * listed in this type. + * + * @example A data provider that handles greeting strings and age numbers (as well as an All data + * type that just provides all the data) could have a DataProviderDataTypes that looks like the + * following: + * + * ```typescript + * { + * Greeting: DataProviderDataType; + * Age: DataProviderDataType; + * All: DataProviderDataType; + * } + * ``` + */ + export type DataProviderDataTypes = { + [dataType: string]: DataProviderDataType; + }; + /** + * 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 + */ + 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 + */ + export type DataProviderSetters = { + [DataType in keyof TDataTypes as `set${DataType & string}`]: DataProviderSetter< + TDataTypes, + DataType + >; + }; + /** + * Set of all `get` methods that a data provider provides according to its data types. + * + * @see DataProviderGetter for more information + */ + export type DataProviderGetters = { + [DataType in keyof TDataTypes as `get${DataType & string}`]: DataProviderGetter< + TDataTypes[DataType] + >; + }; + /** + * Set of all `subscribe` methods that a data provider provides according to its data + * types. + * + * @see DataProviderSubscriber for more information + */ + export type DataProviderSubscribers = { + [DataType in keyof TDataTypes as `subscribe${DataType & string}`]: DataProviderSubscriber< + TDataTypes[DataType] + >; + }; + /** + * An internal object created locally when someone runs dataProviderService.registerEngine. This + * 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 + */ + type DataProviderInternal = + NetworkableObject< + DataProviderSetters & + DataProviderGetters & + DataProviderSubscribers + >; + /** + * Get the data type for a data provider function based on its name + * + * @param fnName Name of data provider function e.g. `getVerse` + * @returns Data type for that data provider function e.g. `Verse` + */ + export function getDataProviderDataTypeFromFunctionName< + TDataTypes extends DataProviderDataTypes = DataProviderDataTypes, + >(fnName: string): DataTypeNames; + export default DataProviderInternal; } -declare module "shared/models/data-provider-engine.model" { - import { DataProviderDataTypes, DataProviderGetters, DataProviderUpdateInstructions, DataProviderSetters } from "shared/models/data-provider.model"; - import { NetworkableObject } from "shared/models/network-object.model"; +declare module 'shared/models/project-data-provider.model' { + import type { DataProviderDataType } 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 + * 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 PDP for accessing extension data. There + * is no way to get or set just a portion of data identified by a single dataQualifier value. + */ + dataQualifier: string; + }; + /** + * 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 + * - 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 + * - In the future, we may enforce that callers to `ExtensionData` endpoints include `extensionName`, + * so following this interface ensures your PDP will not break if such a requirement is + * implemented. + */ + export type MandatoryProjectDataType = { + ExtensionData: DataProviderDataType; + }; +} +declare module 'shared/models/data-provider.interface' { + import { + DataProviderDataTypes, + DataProviderGetters, + DataProviderSetters, + DataProviderSubscribers, + } from 'shared/models/data-provider.model'; + 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. + * + * Note: each `set` method has a corresponding `get` and + * `subscribe` method. + */ + type IDataProvider = + DataProviderSetters & + DataProviderGetters & + DataProviderSubscribers & + OnDidDispose; + export default IDataProvider; + /** + * A data provider that has control over disposing of it with dispose. Returned from registering a + * data provider (only the service that set it up should dispose of it) with + * dataProviderService.registerEngine + * + * @see IDataProvider + */ + export type IDisposableDataProvider> = TDataProvider & + Dispose; +} +declare module 'shared/models/data-provider-engine.model' { + import { + DataProviderDataTypes, + DataProviderGetters, + DataProviderUpdateInstructions, + DataProviderSetters, + } from 'shared/models/data-provider.model'; + import { NetworkableObject } from 'shared/models/network-object.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. + * + * @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: + * + * ```typescript + * notifyUpdate(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 + * + * WARNING: Do not update a data type in its `get` method (unless you make a base case)! + * It will create a destructive infinite loop. + */ + export type DataProviderEngineNotifyUpdate = ( + updateInstructions?: DataProviderUpdateInstructions, + ) => void; + /** + * Addon type for IDataProviderEngine to specify that there is a `notifyUpdate` method on the data + * 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. + */ + export type WithNotifyUpdate = { /** - * 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 @@ -1633,2333 +2018,2897 @@ declare module "shared/models/data-provider-engine.model" { * WARNING: Do not update a data type in its `get` method (unless you make a base case)! * It will create a destructive infinite loop. */ - export type DataProviderEngineNotifyUpdate = (updateInstructions?: DataProviderUpdateInstructions) => void; + notifyUpdate: DataProviderEngineNotifyUpdate; + }; + /** + * 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. + * + * @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 + * Intellisense. + * + * Note: papi creates a `notifyUpdate` function on the 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 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 + * your data provider engine like so: + * ```typescript + * const myDPE: IDataProviderEngine & WithNotifyUpdate = { + * notifyUpdate(updateInstructions) {}, + * ... + * } + * ``` + * OR + * ```typescript + * class MyDPE implements IDataProviderEngine { + * notifyUpdate(updateInstructions?: DataProviderEngineNotifyUpdate) {} + * ... + * } + * ``` + * + * 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 & + /** + * Set of all `set` methods that a data provider engine must provide according to its + * data types. papi overwrites this function on the DataProviderEngine itself to emit an update + * after running the defined `set` method in the DataProviderEngine. + * + * Note: papi requires that each `set` method has a corresponding `get` + * method. + * + * Note: to make a data type read-only, you can always return false or throw from + * `set`. + * + * 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 + */ + DataProviderSetters & + /** + * Set of all `get` methods that a data provider engine must provide according to its + * data types. Run by the data provider on `get` + * + * Note: papi requires that each `set` method has a corresponding `get` + * method. + * + * @see DataProviderGetter for more information + */ + DataProviderGetters & + Partial>; + export default IDataProviderEngine; +} +declare module 'shared/models/extract-data-provider-data-types.model' { + import IDataProviderEngine from 'shared/models/data-provider-engine.model'; + import IDataProvider, { IDisposableDataProvider } from 'shared/models/data-provider.interface'; + import DataProviderInternal from 'shared/models/data-provider.model'; + /** + * Get the `DataProviderDataTypes` associated with the `IDataProvider` - essentially, returns + * `TDataTypes` from `IDataProvider`. + * + * Works with generic types `IDataProvider`, `DataProviderInternal`, `IDisposableDataProvider`, and + * `IDataProviderEngine` along with the `papi-shared-types` extensible interfaces `DataProviders` + * and `DisposableDataProviders` + */ + type ExtractDataProviderDataTypes = + TDataProvider extends IDataProvider + ? TDataProviderDataTypes + : TDataProvider extends DataProviderInternal + ? TDataProviderDataTypes + : TDataProvider extends IDisposableDataProvider + ? TDataProviderDataTypes + : TDataProvider extends IDataProviderEngine + ? TDataProviderDataTypes + : never; + 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 { 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'; + /** + * 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 + * 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 + * following to its `.d.ts` file: + * + * @example + * + * ```typescript + * declare module 'papi-shared-types' { + * export interface CommandHandlers { + * 'myExtension.myCommand1': (foo: string, bar: number) => string; + * 'myExtension.myCommand2': (foo: string) => Promise; + * } + * } + * ``` + */ + interface CommandHandlers { + 'test.echo': (message: string) => string; + 'test.echoRenderer': (message: string) => Promise; + 'test.echoExtensionHost': (message: string) => Promise; + 'test.throwError': (message: string) => void; + 'platform.restartExtensionHost': () => Promise; + 'platform.quit': () => Promise; + 'test.addMany': (...nums: number[]) => number; + 'test.throwErrorExtensionHost': (message: string) => void; + } + /** + * Names for each command available on the papi. + * + * Automatically includes all extensions' commands that are added to {@link CommandHandlers}. + * + * @example 'platform.quit'; + */ + type CommandNames = keyof CommandHandlers; + interface SettingTypes { + 'platform.verseRef': ScriptureReference; + placeholder: undefined; + } + type SettingNames = keyof SettingTypes; + /** This is just a simple example so we have more than one. It's not intended to be real. */ + type NotesOnlyProjectDataTypes = MandatoryProjectDataType & { + 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. + * + * 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. + * + * 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): + * + * @example + * + * ```typescript + * declare module 'papi-shared-types' { + * export type MyProjectDataType = MandatoryProjectDataType & { + * MyProjectData: DataProviderDataType; + * }; + * + * export interface ProjectDataProviders { + * MyExtensionProjectTypeName: IDataProvider; + * } + * } + * ``` + */ + interface ProjectDataProviders { + 'platform.notesOnly': IDataProvider; + 'platform.placeholder': IDataProvider; + } + /** + * Names for each project data provider available on the papi. + * + * Automatically includes all extensions' project data providers that are added to + * {@link ProjectDataProviders}. + * + * @example 'platform.placeholder' + */ + type ProjectTypes = keyof ProjectDataProviders; + /** + * `DataProviderDataTypes` for each project data provider supported by PAPI. These are the data + * types served by each project data provider. + * + * Automatically includes all extensions' project data providers that are added to + * {@link ProjectDataProviders}. + * + * @example + * + * ```typescript + * ProjectDataTypes['MyExtensionProjectTypeName'] => { + * MyProjectData: DataProviderDataType; + * } + * ``` + */ + type ProjectDataTypes = { + [ProjectType in ProjectTypes]: ExtractDataProviderDataTypes; + }; + type StuffDataTypes = { + Stuff: DataProviderDataType; + }; + type PlaceholderDataTypes = { + Placeholder: DataProviderDataType< + { + thing: number; + }, + string[], + number + >; + }; + /** + * `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 + * 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 data provider it registers by + * adding the following to its `.d.ts` file (in this example, we are adding the + * `'helloSomeone.people'` data provider types): + * + * @example + * + * ```typescript + * declare module 'papi-shared-types' { + * export type PeopleDataTypes = { + * Greeting: DataProviderDataType; + * Age: DataProviderDataType; + * People: DataProviderDataType; + * }; + * + * export type PeopleDataMethods = { + * deletePerson(name: string): Promise; + * testRandomMethod(things: string): Promise; + * }; + * + * export type PeopleDataProvider = IDataProvider & PeopleDataMethods; + * + * export interface DataProviders { + * 'helloSomeone.people': PeopleDataProvider; + * } + * } + * ``` + */ + interface DataProviders { + 'platform.stuff': IDataProvider; + 'platform.placeholder': IDataProvider; + } + /** + * Names for each data provider available on the papi. + * + * Automatically includes all extensions' data providers that are added to {@link DataProviders}. + * + * @example 'platform.placeholder' + */ + type DataProviderNames = keyof DataProviders; + /** + * `DataProviderDataTypes` for each data provider supported by PAPI. These are the data types + * served by each data provider. + * + * Automatically includes all extensions' data providers that are added to {@link DataProviders}. + * + * @example + * + * ```typescript + * DataProviderTypes['helloSomeone.people'] => { + * Greeting: DataProviderDataType; + * Age: DataProviderDataType; + * People: DataProviderDataType; + * } + * ``` + */ + type DataProviderTypes = { + [DataProviderName in DataProviderNames]: ExtractDataProviderDataTypes< + DataProviders[DataProviderName] + >; + }; + /** + * Disposable version of each data provider type supported by PAPI. These objects are only + * returned from `papi.dataProviders.registerEngine` - only the one who registers a data provider + * engine is allowed to dispose of the data provider. + * + * Automatically includes all extensions' data providers that are added to {@link DataProviders}. + */ + type DisposableDataProviders = { + [DataProviderName in DataProviderNames]: IDisposableDataProvider< + DataProviders[DataProviderName] + >; + }; +} +declare module 'shared/services/command.service' { + import { UnsubscriberAsync } from 'platform-bible-utils'; + import { CommandHandlers, CommandNames } from 'papi-shared-types'; + module 'papi-shared-types' { + interface CommandHandlers { + 'test.addThree': typeof addThree; + 'test.squareAndConcat': typeof squareAndConcat; + } + } + function addThree(a: number, b: number, c: number): Promise; + function squareAndConcat(a: number, b: string): Promise; + /** Sets up the CommandService. Only runs once and always returns the same promise after that */ + export const initialize: () => Promise; + /** Send a command to the backend. */ + export const sendCommand: ( + commandName: CommandName, + ...args: Parameters + ) => Promise>>; + /** + * Creates a function that is a command function with a baked commandName. This is also nice because + * you get TypeScript type support using this function. + * + * @param commandName Command name for command function + * @returns Function to call with arguments of command that sends the command and resolves with the + * result of the command + */ + export const createSendCommandFunction: ( + commandName: CommandName, + ) => ( + ...args: Parameters + ) => Promise>>; + /** + * Register a command on the papi to be handled here + * + * @param commandName Command name to register for handling here + * + * - Note: Command 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. + * + * @param handler Function to run when the command is invoked + * @returns True if successfully registered, throws with error message if not + */ + export const registerCommand: ( + commandName: CommandName, + handler: CommandHandlers[CommandName], + ) => Promise; + /** + * + * The command service allows you to exchange messages with other components in the platform. You + * can register a command that other services and extensions can send you. You can send commands to + * other services and extensions that have registered commands. + */ + export type moduleSummaryComments = {}; +} +declare module 'shared/models/docking-framework.model' { + import { MutableRefObject, ReactNode } from 'react'; + import { DockLayout, DropDirection, LayoutBase } from 'rc-dock'; + import { + SavedWebViewDefinition, + WebViewDefinition, + WebViewDefinitionUpdateInfo, + } from 'shared/models/web-view.model'; + /** + * Saved information used to recreate a tab. + * + * - {@link TabLoader} loads this into {@link TabInfo} + * - {@link TabSaver} saves {@link TabInfo} into this + */ + export type SavedTabInfo = { + /** + * Tab ID - a unique identifier that identifies this tab. If this tab is a WebView, this ID will + * match the `WebViewDefinition.id` + */ + id: string; + /** Type of tab - indicates what kind of built-in tab this info represents */ + tabType: string; + /** Data needed to load the tab */ + data?: unknown; + }; + /** + * Information that Paranext uses to create a tab in the dock layout. + * + * - {@link TabLoader} loads {@link SavedTabInfo} into this + * - {@link TabSaver} saves this into {@link SavedTabInfo} + */ + export type TabInfo = SavedTabInfo & { + /** + * Url of image to show on the title bar of the tab + * + * Defaults to Platform.Bible logo + */ + tabIconUrl?: string; + /** Text to show on the title bar of the tab */ + tabTitle: string; + /** Text to show when hovering over the title bar of the tab */ + tabTooltip?: string; + /** Content to show inside the tab. */ + content: ReactNode; + /** (optional) Minimum width that the tab can become in CSS `px` units */ + minWidth?: number; + /** (optional) Minimum height that the tab can become in CSS `px` units */ + minHeight?: number; + }; + /** + * Function that takes a {@link SavedTabInfo} and creates a Paranext tab out of it. Each type of tab + * must provide a {@link TabLoader}. + * + * For now all tab creators must do their own data type verification + */ + export type TabLoader = (savedTabInfo: SavedTabInfo) => TabInfo; + /** + * Function that takes a Paranext tab and creates a saved tab out of it. Each type of tab can + * provide a {@link TabSaver}. If they do not provide one, the properties added by `TabInfo` are + * stripped from TabInfo by `saveTabInfoBase` before saving (so it is just a {@link SavedTabInfo}). + * + * @param tabInfo The Paranext tab to save + * @returns The saved tab info for Paranext to persist. If `undefined`, does not save the tab + */ + export type TabSaver = (tabInfo: TabInfo) => SavedTabInfo | undefined; + /** Information about a tab in a panel */ + interface TabLayout { + type: 'tab'; + } + /** + * Indicates where to display a floating window + * + * - `cascade` - place the window a bit below and to the right of the previously created floating + * window + * - `center` - center the window in the dock layout + */ + type FloatPosition = 'cascade' | 'center'; + /** The dimensions for a floating tab in CSS `px` units */ + export type FloatSize = { + width: number; + height: number; + }; + /** Information about a floating window */ + export interface FloatLayout { + type: 'float'; + floatSize?: FloatSize; + /** Where to display the floating window. Defaults to `cascade` */ + position?: FloatPosition; + } + export type PanelDirection = + | 'left' + | 'right' + | 'bottom' + | 'top' + | 'before-tab' + | 'after-tab' + | 'maximize' + | 'move' + | 'active' + | 'update'; + /** Information about a panel */ + interface PanelLayout { + type: 'panel'; + direction?: PanelDirection; + /** If undefined, it will add in the `direction` relative to the previously added tab. */ + targetTabId?: string; + } + /** Information about how a Paranext tab fits into the dock layout */ + export type Layout = TabLayout | FloatLayout | PanelLayout; + /** Event emitted when webViews are created */ + export type AddWebViewEvent = { + webView: SavedWebViewDefinition; + layout: Layout; + }; + /** Props that are passed to the web view tab component */ + export type WebViewTabProps = WebViewDefinition; + /** Rc-dock's onLayoutChange prop made asynchronous - resolves */ + export type OnLayoutChangeRCDock = ( + newLayout: LayoutBase, + currentTabId?: string, + direction?: DropDirection, + ) => Promise; + /** Properties related to the dock layout */ + export type PapiDockLayout = { + /** The rc-dock dock layout React element ref. Used to perform operations on the layout */ + dockLayout: DockLayout; + /** + * A ref to a function that runs when the layout changes. We set this ref to our + * {@link onLayoutChange} function + */ + onLayoutChangeRef: MutableRefObject; + /** + * Add or update a tab in the layout + * + * @param savedTabInfo Info for tab to add or update + * @param layout Information about where to put a new tab + * @returns If tab added, final layout used to display the new tab. If existing tab updated, + * `undefined` + */ + addTabToDock: (savedTabInfo: SavedTabInfo, layout: Layout) => Layout | undefined; + /** + * Add or update a webview in the layout + * + * @param webView Web view to add or update + * @param layout Information about where to put a new webview + * @returns If WebView added, final layout used to display the new webView. If existing webView + * updated, `undefined` + */ + addWebViewToDock: (webView: WebViewTabProps, layout: Layout) => Layout | undefined; + /** + * Remove a tab in the layout + * + * @param tabId ID of the tab to remove + */ + removeTabFromDock: (tabId: string) => boolean; + /** + * Gets the WebView definition for the web view with the specified ID + * + * @param webViewId The ID of the WebView whose web view definition to get + * @returns WebView definition with the specified ID or undefined if not found + */ + getWebViewDefinition: (webViewId: string) => WebViewDefinition | undefined; + /** + * Updates the WebView with the specified ID with the specified properties + * + * @param webViewId The ID of the WebView to update + * @param updateInfo Properties to update on the WebView. Any unspecified properties will stay the + * same + * @returns True if successfully found the WebView to update; false otherwise + */ + updateWebViewDefinition: ( + webViewId: string, + updateInfo: WebViewDefinitionUpdateInfo, + ) => boolean; + /** + * The layout to use as the default layout if the dockLayout doesn't have a layout loaded. + * + * TODO: This should be removed and the `testLayout` imported directly in this file once this + * service is refactored to split the code between processes. The only reason this is passed from + * `platform-dock-layout.component.tsx` is that we cannot import `testLayout` here since this + * service is currently all shared code. Refactor should happen in #203 + */ + testLayout: LayoutBase; + }; +} +declare module 'shared/services/web-view.service-model' { + import { GetWebViewOptions, WebViewId, WebViewType } from 'shared/models/web-view.model'; + import { AddWebViewEvent, Layout } from 'shared/models/docking-framework.model'; + import { PlatformEvent } from 'platform-bible-utils'; + /** + * + * Service exposing various functions related to using webViews + * + * WebViews are iframes in the Platform.Bible UI into which extensions load frontend code, either + * HTML or React components. + */ + export interface WebViewServiceType { + /** Event that emits with webView info when a webView is added */ + onDidAddWebView: PlatformEvent; + /** + * Creates a new web view or gets an existing one depending on if you request an existing one and + * if the web view provider decides to give that existing one to you (it is up to the provider). + * + * @param webViewType Type of WebView to create + * @param layout Information about where you want the web view to go. Defaults to adding as a tab + * @param options Options that affect what this function does. For example, you can provide an + * existing web view ID to request an existing web view with that ID. + * @returns Promise that resolves to the ID of the webview we got or undefined if the provider did + * not create a WebView for this request. + * @throws If something went wrong like the provider for the webViewType was not found + */ + getWebView: ( + webViewType: WebViewType, + layout?: Layout, + options?: GetWebViewOptions, + ) => Promise; + } + /** Name to use when creating a network event that is fired when webViews are created */ + export const EVENT_NAME_ON_DID_ADD_WEB_VIEW: `${string}:${string}`; + export const NETWORK_OBJECT_NAME_WEB_VIEW_SERVICE = 'WebViewService'; +} +declare module 'shared/services/network-object-status.service-model' { + import { NetworkObjectDetails } from 'shared/models/network-object.model'; + export interface NetworkObjectStatusRemoteServiceType { /** - * Addon type for IDataProviderEngine to specify that there is a `notifyUpdate` method on the data - * 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`. + * Get details about all available network objects * - * @see DataProviderEngineNotifyUpdate for more information on `notifyUpdate`. - * @see IDataProviderEngine for more information on using this type. + * @returns Object whose keys are the names of the network objects and whose values are the + * {@link NetworkObjectDetails} for each network object */ - export type WithNotifyUpdate = { - /** JSDOC DESTINATION DataProviderEngineNotifyUpdate */ - notifyUpdate: DataProviderEngineNotifyUpdate; - }; + getAllNetworkObjectDetails: () => Promise>; + } + /** Provides functions related to the set of available network objects */ + export interface NetworkObjectStatusServiceType extends NetworkObjectStatusRemoteServiceType { /** - * 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. - * - * @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 - * Intellisense. - * - * Note: papi creates a `notifyUpdate` function on the 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 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 - * your data provider engine like so: - * ```typescript - * const myDPE: IDataProviderEngine & WithNotifyUpdate = { - * notifyUpdate(updateInstructions) {}, - * ... - * } - * ``` - * OR - * ```typescript - * class MyDPE implements IDataProviderEngine { - * notifyUpdate(updateInstructions?: DataProviderEngineNotifyUpdate) {} - * ... - * } - * ``` + * Get a promise that resolves when a network object is registered or rejects if a timeout is hit * - * 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 { - * ... - * } - * ``` + * @returns Promise that either resolves to the {@link NetworkObjectDetails} for a network object + * once the network object is registered, or rejects if a timeout is provided and the timeout is + * reached before the network object is registered */ - type IDataProviderEngine = NetworkableObject & + waitForNetworkObject: (id: string, timeoutInMS?: number) => Promise; + } + export const networkObjectStatusServiceNetworkObjectName = 'NetworkObjectStatusService'; +} +declare module 'shared/services/network-object-status.service' { + import { NetworkObjectStatusServiceType } from 'shared/services/network-object-status.service-model'; + const networkObjectStatusService: NetworkObjectStatusServiceType; + export default networkObjectStatusService; +} +declare module 'shared/services/web-view.service' { + import { WebViewServiceType } from 'shared/services/web-view.service-model'; + const webViewService: WebViewServiceType; + export default webViewService; +} +declare module 'shared/models/web-view-provider.model' { + import { + GetWebViewOptions, + WebViewDefinition, + SavedWebViewDefinition, + } from 'shared/models/web-view.model'; + import { + DisposableNetworkObject, + NetworkObject, + NetworkableObject, + } from 'shared/models/network-object.model'; + import { CanHaveOnDidDispose } from 'platform-bible-utils'; + export interface IWebViewProvider extends NetworkableObject { + /** + * @param savedWebView Filled out if an existing webview is being called for (matched by ID). Just + * ID if this is a new request or if the web view with the existing ID was not found + * @param getWebViewOptions + */ + getWebView( + savedWebView: SavedWebViewDefinition, + getWebViewOptions: GetWebViewOptions, + ): Promise; + } + export interface WebViewProvider + extends NetworkObject, + CanHaveOnDidDispose {} + export interface DisposableWebViewProvider + extends DisposableNetworkObject, + Omit {} +} +declare module 'shared/services/web-view-provider.service' { + /** + * Handles registering web view providers and serving web views around the papi. Exposed on the + * papi. + */ + import { + DisposableWebViewProvider, + IWebViewProvider, + WebViewProvider, + } from 'shared/models/web-view-provider.model'; + /** Sets up the service. Only runs once and always returns the same promise after that */ + const initialize: () => Promise; + /** + * Indicate if we are aware of an existing web view provider with the given type. If a web view + * provider with the given type 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. + * + * @param webViewType Type of webView to check for + */ + function hasKnown(webViewType: string): boolean; + /** + * Register a web view provider to serve webViews for a specified type of webViews + * + * @param webViewType Type of web view to provide + * @param webViewProvider Object to register as a webView provider including control over disposing + * of it. + * + * WARNING: setting a webView provider mutates the provided object. + * @returns `webViewProvider` modified to be a network object + */ + function register( + webViewType: string, + webViewProvider: IWebViewProvider, + ): Promise; + /** + * Get a web view provider that has previously been set up + * + * @param webViewType Type of webview provider to get + * @returns Web view provider with the given name if one exists, undefined otherwise + */ + function get(webViewType: string): Promise; + export interface WebViewProviderService { + initialize: typeof initialize; + hasKnown: typeof hasKnown; + register: typeof register; + get: typeof get; + } + export interface PapiWebViewProviderService { + register: typeof register; + } + const webViewProviderService: WebViewProviderService; + /** + * + * Interface for registering webView providers + */ + export const papiWebViewProviderService: PapiWebViewProviderService; + export default webViewProviderService; +} +declare module 'shared/services/internet.service' { + /** Our shim over fetch. Allows us to control internet access. */ + const papiFetch: typeof fetch; + export interface InternetService { + fetch: typeof papiFetch; + } + /** + * + * Service that provides a way to call `fetch` since the original function is not available + */ + const internetService: InternetService; + export default internetService; +} +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, + } from 'shared/models/data-provider-engine.model'; + import { + DataProviderNames, + DataProviderTypes, + DataProviders, + DisposableDataProviders, + } from 'papi-shared-types'; + 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 + * + * @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 + */ + function ignore( + method: Function & { + isIgnored?: boolean; + }, + ): void; + /** + * 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 + * + * @param target The class that has the method to ignore + * @param member The name of the method to ignore + * + * 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; + /** + * 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. + */ + const decorators: { + ignore: typeof ignore; + }; + /** + * 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. + */ + function registerEngine( + providerName: DataProviderName, + dataProviderEngine: IDataProviderEngine, + dataProviderType?: string, + dataProviderAttributes?: + | { + [property: string]: unknown; + } + | undefined, + ): Promise; + /** + * Creates a data provider to be shared on the network layering over the provided data provider + * engine. + * + * @type `TDataTypes` - The data provider data types served by the data provider to create. + * + * This is not exposed on the papi as it is a helper function to enable other services to layer over + * this service and create their own subsets of data providers with other types than + * `DataProviders` types using this function and {@link getByType} + * @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. + */ + export function registerEngineByType( + providerName: string, + dataProviderEngine: IDataProviderEngine, + dataProviderType?: string, + dataProviderAttributes?: + | { + [property: string]: unknown; + } + | undefined, + ): Promise>>; + /** + * 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 + */ + function get( + providerName: DataProviderName, + ): Promise; + /** + * Get a data provider that has previously been set up + * + * @type `T` - The type of data provider to get. Use `IDataProvider`, + * specifying your own types, or provide a custom data provider type + * + * This is not exposed on the papi as it is a helper function to enable other services to layer over + * this service and create their own subsets of data providers with other types than + * `DataProviders` types using this function and {@link registerEngineByType} + * @param providerName Name of the desired data provider + * @returns The data provider with the given name if one exists, undefined otherwise + */ + export function getByType>( + providerName: string, + ): Promise; + export interface DataProviderService { + hasKnown: typeof hasKnown; + registerEngine: typeof registerEngine; + get: typeof get; + decorators: typeof decorators; + DataProviderEngine: typeof DataProviderEngine; + } + /** + * + * Service that allows extensions to send and receive data to/from other extensions + */ + const dataProviderService: DataProviderService; + 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 */ + export type ProjectDataProviderEngineTypes = { + [ProjectType in ProjectTypes]: IDataProviderEngine; + }; + export interface ProjectDataProviderEngineFactory { + createProjectDataProviderEngine( + projectId: string, + projectStorageInterpreterId: string, + ): ProjectDataProviderEngineTypes[ProjectType]; + } +} +declare module 'shared/models/project-metadata.model' { + import { ProjectTypes } from 'papi-shared-types'; + /** + * Low-level information describing a project that Platform.Bible directly manages and uses to load + * project data + */ + export type ProjectMetadata = { + /** ID of the project (must be unique and case insensitive) */ + id: string; + /** Short name of the project (not necessarily unique) */ + name: string; + /** Indicates how the project is persisted to storage */ + storageType: string; + /** + * Indicates what sort of project this is which implies its data shape (e.g., what data streams + * should be available) + */ + projectType: ProjectTypes; + }; +} +declare module 'shared/services/project-lookup.service-model' { + import { ProjectMetadata } from 'shared/models/project-metadata.model'; + /** + * + * Provides metadata for projects known by the platform + */ + export interface ProjectLookupServiceType { /** - * Set of all `set` methods that a data provider engine must provide according to its - * data types. papi overwrites this function on the DataProviderEngine itself to emit an update - * after running the defined `set` method in the DataProviderEngine. - * - * Note: papi requires that each `set` method has a corresponding `get` - * method. + * Provide metadata for all projects found on the local system * - * Note: to make a data type read-only, you can always return false or throw from - * `set`. - * - * 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 + * @returns ProjectMetadata for all projects stored on the local system */ - DataProviderSetters & + getMetadataForAllProjects: () => Promise; /** - * Set of all `get` methods that a data provider engine must provide according to its - * data types. Run by the data provider on `get` + * Look up metadata for a specific project ID * - * Note: papi requires that each `set` method has a corresponding `get` - * method. - * - * @see DataProviderGetter for more information + * @param projectId ID of the project to load + * @returns ProjectMetadata from the 'meta.json' file for the given project */ - DataProviderGetters & Partial>; - export default IDataProviderEngine; + getMetadataForProject: (projectId: string) => Promise; + } + export const projectLookupServiceNetworkObjectName = 'ProjectLookupService'; } -declare module "shared/models/extract-data-provider-data-types.model" { - import IDataProviderEngine from "shared/models/data-provider-engine.model"; - import IDataProvider, { IDisposableDataProvider } from "shared/models/data-provider.interface"; - import DataProviderInternal from "shared/models/data-provider.model"; - /** - * Get the `DataProviderDataTypes` associated with the `IDataProvider` - essentially, returns - * `TDataTypes` from `IDataProvider`. - * - * Works with generic types `IDataProvider`, `DataProviderInternal`, `IDisposableDataProvider`, and - * `IDataProviderEngine` along with the `papi-shared-types` extensible interfaces `DataProviders` - * and `DisposableDataProviders` - */ - type ExtractDataProviderDataTypes = TDataProvider extends IDataProvider ? TDataProviderDataTypes : TDataProvider extends DataProviderInternal ? TDataProviderDataTypes : TDataProvider extends IDisposableDataProvider ? TDataProviderDataTypes : TDataProvider extends IDataProviderEngine ? TDataProviderDataTypes : never; - export default ExtractDataProviderDataTypes; +declare module 'shared/services/project-lookup.service' { + import { ProjectLookupServiceType } from 'shared/services/project-lookup.service-model'; + const projectLookupService: ProjectLookupServiceType; + export default projectLookupService; } -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 { 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"; - /** - * 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 - * 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 - * following to its `.d.ts` file: - * - * @example - * - * ```typescript - * declare module 'papi-shared-types' { - * export interface CommandHandlers { - * 'myExtension.myCommand1': (foo: string, bar: number) => string; - * 'myExtension.myCommand2': (foo: string) => Promise; - * } - * } - * ``` - */ - interface CommandHandlers { - 'test.echo': (message: string) => string; - 'test.echoRenderer': (message: string) => Promise; - 'test.echoExtensionHost': (message: string) => Promise; - 'test.throwError': (message: string) => void; - 'platform.restartExtensionHost': () => Promise; - 'platform.quit': () => Promise; - 'test.addMany': (...nums: number[]) => number; - 'test.throwErrorExtensionHost': (message: string) => void; +declare module 'shared/services/project-data-provider.service' { + import { ProjectTypes, ProjectDataProviders } from 'papi-shared-types'; + import { ProjectDataProviderEngineFactory } from 'shared/models/project-data-provider-engine.model'; + import { Dispose } from 'platform-bible-utils'; + /** + * Add a new Project Data Provider Factory to PAPI that uses the given engine. There must not be an + * existing factory already that handles the same project type or this operation will fail. + * + * @param projectType Type of project that pdpEngineFactory supports + * @param pdpEngineFactory Used in a ProjectDataProviderFactory to create ProjectDataProviders + * @returns Promise that resolves to a disposable object when the registration operation completes + */ + export function registerProjectDataProviderEngineFactory( + projectType: ProjectType, + pdpEngineFactory: ProjectDataProviderEngineFactory, + ): Promise; + /** + * Get a Project Data Provider for the given project ID. + * + * @example + * + * ```typescript + * const pdp = await get('ParatextStandard', 'ProjectID12345'); + * pdp.getVerse(new VerseRef('JHN', '1', '1')); + * ``` + * + * @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 + * project's actual `projectType` (according to its metadata), a warning will be logged + * @param projectId ID for the project to load + * @returns Data provider with types that are associated with the given project type + */ + export function get( + projectType: ProjectType, + projectId: string, + ): Promise; + export interface PapiBackendProjectDataProviderService { + registerProjectDataProviderEngineFactory: typeof registerProjectDataProviderEngineFactory; + get: typeof get; + } + /** + * + * Service that registers and gets project data providers + */ + export const papiBackendProjectDataProviderService: PapiBackendProjectDataProviderService; + export interface PapiFrontendProjectDataProviderService { + get: typeof get; + } + /** + * + * Service that gets project data providers + */ + export const papiFrontendProjectDataProviderService: { + get: typeof get; + }; +} +declare module 'shared/data/file-system.model' { + /** Types to use with file system operations */ + /** + * Represents a path in file system or other. Has a scheme followed by :// followed by a relative + * path. If no scheme is provided, the app scheme is used. Available schemes are as follows: + * + * - `app://` - goes to the app's home directory and into `.platform.bible` (platform-dependent) + * - `cache://` - goes to the app's temporary file cache at `app://cache` + * - `data://` - goes to the app's data storage location at `app://data` + * - `resources://` - goes to the resources directory installed in the app + * - `file://` - an absolute file path from root + */ + export type Uri = string; +} +declare module 'node/utils/util' { + import { Uri } from 'shared/data/file-system.model'; + export const FILE_PROTOCOL = 'file://'; + export const RESOURCES_PROTOCOL = 'resources://'; + export function resolveHtmlPath(htmlFileName: string): string; + /** + * Gets the platform-specific user Platform.Bible folder for this application + * + * When running in development: `/dev-appdata` + * + * When packaged: `/.platform.bible` + */ + export const getAppDir: import('memoize-one').MemoizedFn<() => string>; + /** + * Resolves the uri to a path + * + * @param uri The uri to resolve + * @returns Real path to the uri supplied + */ + export function getPathFromUri(uri: Uri): string; + /** + * Combines the uri passed in with the paths passed in to make one uri + * + * @param uri Uri to start from + * @param paths Paths to combine into the uri + * @returns One uri that combines the uri and the paths in left-to-right order + */ + export function joinUriPaths(uri: Uri, ...paths: string[]): Uri; +} +declare module 'node/services/node-file-system.service' { + /** File system calls from Node */ + import fs, { BigIntStats } from 'fs'; + import { Uri } from 'shared/data/file-system.model'; + /** + * Read a text file + * + * @param uri URI of file + * @returns Promise that resolves to the contents of the file + */ + export function readFileText(uri: Uri): Promise; + /** + * Read a binary file + * + * @param uri URI of file + * @returns Promise that resolves to the contents of the file + */ + export function readFileBinary(uri: Uri): Promise; + /** + * Write data to a file + * + * @param uri URI of file + * @param fileContents String or Buffer to write into the file + * @returns Promise that resolves after writing the file + */ + export function writeFile(uri: Uri, fileContents: string | Buffer): Promise; + /** + * Copies a file from one location to another. Creates the path to the destination if it does not + * exist + * + * @param sourceUri The location of the file to copy + * @param destinationUri The uri to the file to create as a copy of the source file + * @param mode Bitwise modifiers that affect how the copy works. See + * [`fsPromises.copyFile`](https://nodejs.org/api/fs.html#fspromisescopyfilesrc-dest-mode) for + * more information + */ + export function copyFile( + sourceUri: Uri, + destinationUri: Uri, + mode?: Parameters[2], + ): Promise; + /** + * Delete a file if it exists + * + * @param uri URI of file + * @returns Promise that resolves when the file is deleted or determined to not exist + */ + export function deleteFile(uri: Uri): Promise; + /** + * Get stats about the file or directory. Note that BigInts are used instead of ints to avoid. + * https://en.wikipedia.org/wiki/Year_2038_problem + * + * @param uri URI of file or directory + * @returns Promise that resolves to object of type https://nodejs.org/api/fs.html#class-fsstats if + * file or directory exists, undefined if it doesn't + */ + export function getStats(uri: Uri): Promise; + /** + * Set the last modified and accessed times for the file or directory + * + * @param uri URI of file or directory + * @returns Promise that resolves once the touch operation finishes + */ + export function touch(uri: Uri, date: Date): Promise; + /** Type of file system item in a directory */ + export enum EntryType { + File = 'file', + Directory = 'directory', + Unknown = 'unknown', + } + /** All entries in a directory, mapped from entry type to array of uris for the entries */ + export type DirectoryEntries = Readonly<{ + [entryType in EntryType]: Uri[]; + }>; + /** + * Reads a directory and returns lists of entries in the directory by entry type. + * + * @param uri - URI of directory. + * @param entryFilter - Function to filter out entries in the directory based on their names. + * @returns Map of entry type to list of uris for each entry in the directory with that type. + */ + export function readDir( + uri: Uri, + entryFilter?: (entryName: string) => boolean, + ): Promise; + /** + * Create a directory in the file system if it does not exist. Does not throw if it already exists. + * + * @param uri URI of directory + * @returns Promise that resolves once the directory has been created + */ + export function createDir(uri: Uri): Promise; + /** + * Remove a directory and all its contents recursively from the file system + * + * @param uri URI of directory + * @returns Promise that resolves when the delete operation finishes + */ + export function deleteDir(uri: Uri): Promise; +} +declare module 'node/utils/crypto-util' { + export function createUuid(): string; + /** + * Create a cryptographically secure nonce that is at least 128 bits long. See nonce spec at + * https://w3c.github.io/webappsec-csp/#security-nonces + * + * @param encoding: "base64url" (HTML safe, shorter string) or "hex" (longer string) From + * https://base64.guru/standards/base64url, the purpose of this encoding is "the ability to use + * the encoding result as filename or URL address" + * @param numberOfBytes: Number of bytes the resulting nonce should contain + * @returns Cryptographically secure, pseudo-randomly generated value encoded as a string + */ + export function createNonce(encoding: 'base64url' | 'hex', numberOfBytes?: number): string; +} +declare module 'node/models/execution-token.model' { + /** For now this is just for extensions, but maybe we will want to expand this in the future */ + export type ExecutionTokenType = 'extension'; + /** Execution tokens can be passed into API calls to provide context about their identity */ + export class ExecutionToken { + readonly type: ExecutionTokenType; + readonly name: string; + readonly nonce: string; + constructor(tokenType: ExecutionTokenType, name: string); + getHash(): string; + } +} +declare module 'node/services/execution-token.service' { + import { ExecutionToken } from 'node/models/execution-token.model'; + /** + * This should be called when extensions are being loaded + * + * @param extensionName Name of the extension to register + * @returns Token that can be passed to `tokenIsValid` to authenticate or authorize API callers. It + * is important that the token is not shared to avoid impersonation of API callers. + */ + function registerExtension(extensionName: string): ExecutionToken; + /** + * Remove a registered token. Note that a hash of a token is what is needed to unregister, not the + * full token itself (notably not the nonce), so something can be delegated the ability to + * unregister a token without having been given the full token itself. + * + * @param extensionName Name of the extension that was originally registered + * @param tokenHash Value of `getHash()` of the token that was originally registered. + * @returns `true` if the token was successfully unregistered, `false` otherwise + */ + function unregisterExtension(extensionName: string, tokenHash: string): boolean; + /** + * This should only be needed by services that need to contextualize the response for the caller + * + * @param executionToken Token that was previously registered. + * @returns `true` if the token matches a token that was previous registered, `false` otherwise. + */ + function tokenIsValid(executionToken: ExecutionToken): boolean; + const executionTokenService: { + registerExtension: typeof registerExtension; + unregisterExtension: typeof unregisterExtension; + tokenIsValid: typeof tokenIsValid; + }; + export default executionTokenService; +} +declare module 'extension-host/services/extension-storage.service' { + import { ExecutionToken } from 'node/models/execution-token.model'; + import { Buffer } from 'buffer'; + /** + * This is only intended to be called by the extension service. This service cannot call into the + * extension service or it causes a circular dependency. + */ + export function setExtensionUris(urisPerExtension: Map): void; + /** Return a path to the specified file within the extension's installation directory */ + export function buildExtensionPathFromName(extensionName: string, fileName: string): string; + /** + * Read a text file from the the extension's installation directory + * + * @param token ExecutionToken provided to the extension when `activate()` was called + * @param fileName Name of the file to be read + * @returns Promise for a string with the contents of the file + */ + function readTextFileFromInstallDirectory( + token: ExecutionToken, + fileName: string, + ): Promise; + /** + * Read a binary file from the the extension's installation directory + * + * @param token ExecutionToken provided to the extension when `activate()` was called + * @param fileName Name of the file to be read + * @returns Promise for a Buffer with the contents of the file + */ + function readBinaryFileFromInstallDirectory( + token: ExecutionToken, + fileName: string, + ): Promise; + /** + * Read data specific to the user (as identified by the OS) and extension (as identified by the + * ExecutionToken) + * + * @param token ExecutionToken provided to the extension when `activate()` was called + * @param key Unique identifier of the data + * @returns Promise for a string containing the data + */ + function readUserData(token: ExecutionToken, key: string): Promise; + /** + * Write data specific to the user (as identified by the OS) and extension (as identified by the + * ExecutionToken) + * + * @param token ExecutionToken provided to the extension when `activate()` was called + * @param key Unique identifier of the data + * @param data Data to be written + * @returns Promise that will resolve if the data is written successfully + */ + function writeUserData(token: ExecutionToken, key: string, data: string): Promise; + /** + * Delete data previously written that is specific to the user (as identified by the OS) and + * extension (as identified by the ExecutionToken) + * + * @param token ExecutionToken provided to the extension when `activate()` was called + * @param key Unique identifier of the data + * @returns Promise that will resolve if the data is deleted successfully + */ + function deleteUserData(token: ExecutionToken, key: string): Promise; + export interface ExtensionStorageService { + readTextFileFromInstallDirectory: typeof readTextFileFromInstallDirectory; + readBinaryFileFromInstallDirectory: typeof readBinaryFileFromInstallDirectory; + readUserData: typeof readUserData; + writeUserData: typeof writeUserData; + deleteUserData: typeof deleteUserData; + } + /** + * + * This service provides extensions in the extension host the ability to read/write data based on + * the extension identity and current user (as identified by the OS). This service will not work + * within the renderer. + */ + const extensionStorageService: ExtensionStorageService; + export default extensionStorageService; +} +declare module 'shared/models/dialog-options.model' { + /** General options to adjust dialogs (created from `papi.dialogs`) */ + export type DialogOptions = { + /** Dialog title to display in the header. Default depends on the dialog */ + title?: string; + /** Url of dialog icon to display in the header. Default is Platform.Bible logo */ + iconUrl?: string; + /** The message to show the user in the dialog. Default depends on the dialog */ + prompt?: string; + }; + /** Data in each tab that is a dialog. Added to DialogOptions in `dialog.service-host.ts` */ + export type DialogData = DialogOptions & { + isDialog: true; + }; +} +declare module 'renderer/components/dialogs/dialog-base.data' { + import { FloatSize, TabLoader, TabSaver } from 'shared/models/docking-framework.model'; + import { DialogData } from 'shared/models/dialog-options.model'; + import { ReactElement } from 'react'; + /** Base type for DialogDefinition. Contains reasonable defaults for dialogs */ + export type DialogDefinitionBase = Readonly<{ + /** Overwritten in {@link DialogDefinition}. Must be specified by all DialogDefinitions */ + tabType?: string; + /** Overwritten in {@link DialogDefinition}. Must be specified by all DialogDefinitions */ + Component?: (props: DialogProps) => ReactElement; + /** + * The default icon for this dialog. This may be overridden by the `DialogOptions.iconUrl` + * + * Defaults to the Platform.Bible logo + */ + defaultIconUrl?: string; + /** + * The default title for this dialog. This may be overridden by the `DialogOptions.title` + * + * Defaults to the DialogDefinition's `tabType` + */ + defaultTitle?: string; + /** The width and height at which the dialog will be loaded in CSS `px` units */ + initialSize: FloatSize; + /** The minimum width to which the dialog can be set in CSS `px` units */ + minWidth?: number; + /** The minimum height to which the dialog can be set in CSS `px` units */ + minHeight?: number; + /** + * The function used to load the dialog into the dock layout. Default uses the `Component` field + * and passes in the `DialogProps` + */ + loadDialog: TabLoader; + /** + * The function used to save the dialog into the dock layout + * + * Default does not save the dialog as they cannot properly be restored yet. + * + * TODO: preserve requests between refreshes - save the dialog info in such a way that it works + * when loading again after refresh + */ + saveDialog: TabSaver; + }>; + /** Props provided to the dialog component */ + export type DialogProps = DialogData & { + /** + * Sends the data as a resolved response to the dialog request and closes the dialog + * + * @param data Data with which to resolve the request + */ + submitDialog(data: TData): void; + /** Cancels the dialog request (resolves the response with `undefined`) and closes the dialog */ + cancelDialog(): void; + /** + * Rejects the dialog request with the specified message and closes the dialog + * + * @param errorMessage Message to explain why the dialog request was rejected + */ + rejectDialog(errorMessage: string): void; + }; + /** + * Set the functionality of submitting and canceling dialogs. This should be called specifically by + * `dialog.service-host.ts` immediately on startup and by nothing else. This is only here to + * mitigate a dependency cycle + * + * @param dialogServiceFunctions Functions from the dialog service host for resolving and rejecting + * dialogs + */ + export function hookUpDialogService({ + resolveDialogRequest: resolve, + rejectDialogRequest: reject, + }: { + resolveDialogRequest: (id: string, data: unknown | undefined) => void; + rejectDialogRequest: (id: string, message: string) => void; + }): void; + /** + * Static definition of a dialog that can be shown in Platform.Bible + * + * For good defaults, dialogs can include all the properties of this dialog. Dialogs must then + * specify `tabType` and `Component` in order to comply with `DialogDefinition` + * + * Note: this is not a class that can be inherited because all properties would be static but then + * we would not be able to use the default `loadDialog` because it would be using a static reference + * to a nonexistent `Component`. Instead of inheriting this as a class, any dialog definition can + * spread this `{ ...DIALOG_BASE }` + */ + const DIALOG_BASE: DialogDefinitionBase; + export default DIALOG_BASE; +} +declare module 'renderer/components/dialogs/dialog-definition.model' { + import { DialogOptions } from 'shared/models/dialog-options.model'; + import { DialogDefinitionBase, DialogProps } from 'renderer/components/dialogs/dialog-base.data'; + import { ReactElement } from 'react'; + /** The tabType for the select project dialog in `select-project.dialog.tsx` */ + export const SELECT_PROJECT_DIALOG_TYPE = 'platform.selectProject'; + /** The tabType for the select multiple projects dialog in `select-multiple-projects.dialog.tsx` */ + export const SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE = 'platform.selectMultipleProjects'; + /** Options to provide when showing the Select Project dialog */ + export type SelectProjectDialogOptions = DialogOptions & { + /** Project IDs to exclude from showing in the dialog */ + excludeProjectIds?: string[]; + }; + /** Options to provide when showing the Select Multiple Project dialog */ + export type SelectMultipleProjectsDialogOptions = DialogOptions & { + /** Project IDs to exclude from showing in the dialog */ + excludeProjectIds?: string[]; + /** Project IDs that should start selected in the dialog */ + selectedProjectIds?: string[]; + }; + /** + * Mapped type for dialog functions to use in getting various types for dialogs + * + * Keys should be dialog names, and values should be {@link DialogDataTypes} + * + * If you add a dialog here, you must also add it on {@link DIALOGS} + */ + export interface DialogTypes { + [SELECT_PROJECT_DIALOG_TYPE]: DialogDataTypes; + [SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE]: DialogDataTypes< + SelectMultipleProjectsDialogOptions, + string[] + >; + } + /** Each type of dialog. These are the tab types used in the dock layout */ + export type DialogTabTypes = keyof DialogTypes; + /** Types related to a specific dialog */ + export type DialogDataTypes = { + /** + * The dialog options to specify when calling the dialog. Passed into `loadDialog` as + * SavedTabInfo.data + * + * The default implementation of `loadDialog` passes all the options down to the dialog component + * as props + */ + options: TOptions; + /** The type of the response to the dialog request */ + responseType: TReturnType; + /** Props provided to the dialog component */ + props: DialogProps & TOptions; + }; + export type DialogDefinition = Readonly< + DialogDefinitionBase & { + /** + * Type of tab - indicates what kind of built-in dock layout tab this dialog definition + * represents + */ + tabType: DialogTabType; + /** + * React component to render for this dialog + * + * This must be specified only if you do not overwrite the default `loadDialog` + * + * @param props Props that will be passed through from the dialog tab's data + * @returns React element to render + */ + Component: ( + props: DialogProps & + DialogTypes[DialogTabType]['options'], + ) => ReactElement; } - /** - * Names for each command available on the papi. - * - * Automatically includes all extensions' commands that are added to {@link CommandHandlers}. - * - * @example 'platform.quit'; - */ - type CommandNames = keyof CommandHandlers; - interface SettingTypes { - 'platform.verseRef': ScriptureReference; - placeholder: undefined; + >; +} +declare module 'shared/services/dialog.service-model' { + import { DialogTabTypes, DialogTypes } from 'renderer/components/dialogs/dialog-definition.model'; + import { DialogOptions } from 'shared/models/dialog-options.model'; + /** + * + * Prompt the user for responses with dialogs + */ + export interface DialogService { + /** + * Shows a dialog to the user and prompts the user to respond + * + * @type `TReturn` - The type of data the dialog responds with + * @param dialogType The type of dialog to show the user + * @param options Various options for configuring the dialog that shows + * @returns Returns the user's response or `undefined` if the user cancels + */ + showDialog( + dialogType: DialogTabType, + options?: DialogTypes[DialogTabType]['options'], + ): Promise; + /** + * Shows a select project dialog to the user and prompts the user to select a dialog + * + * @param options Various options for configuring the dialog that shows + * @returns Returns the user's selected project id or `undefined` if the user cancels + */ + selectProject(options?: DialogOptions): Promise; + } + /** Prefix on requests that indicates that the request is related to dialog operations */ + export const CATEGORY_DIALOG = 'dialog'; +} +declare module 'shared/services/dialog.service' { + import { DialogService } from 'shared/services/dialog.service-model'; + const dialogService: DialogService; + export default dialogService; +} +declare module 'extension-host/extension-types/extension-activation-context.model' { + import { ExecutionToken } from 'node/models/execution-token.model'; + import { UnsubscriberAsyncList } from 'platform-bible-utils'; + /** An object of this type is passed into `activate()` for each extension during initialization */ + export type ExecutionActivationContext = { + /** Canonical name of the extension */ + name: string; + /** Used to save and load data from the storage service. */ + executionToken: ExecutionToken; + /** Tracks all registrations made by an extension so they can be cleaned up when it is unloaded */ + registrations: UnsubscriberAsyncList; + }; +} +declare module 'renderer/hooks/papi-hooks/use-dialog-callback.hook' { + import { DialogTabTypes, DialogTypes } from 'renderer/components/dialogs/dialog-definition.model'; + export type UseDialogCallbackOptions = { + /** + * How many dialogs are allowed to be open at once from this dialog callback. Calling the callback + * when this number of maximum open dialogs has been reached does nothing. Set to -1 for + * unlimited. Defaults to 1. + */ + maximumOpenDialogs?: number; + }; + /** + * + * Enables using `papi.dialogs.showDialog` in React more easily. Returns a callback to run that will + * open a dialog with the provided `dialogType` and `options` then run the `resolveCallback` with + * the dialog response or `rejectCallback` if there is an error. By default, only one dialog can be + * open at a time. + * + * If you need to open multiple dialogs and track which dialog is which, you can set + * `options.shouldOpenMultipleDialogs` to `true` and add a counter to the `options` when calling the + * callback. Then `resolveCallback` will be resolved with that options object including your + * counter. + * + * @type `DialogTabType` The dialog type you are using. Should be inferred by parameters + * @param dialogType Dialog type you want to show on the screen + * + * 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 updating this parameter will not cause a new + * callback to be returned. However, because of the nature of calling dialogs, this has no adverse + * effect on the functionality of this hook. Calling the callback will always use the latest + * `dialogType`. + * @param options Various options for configuring the dialog that shows and this hook. If an + * `options` parameter is also provided to the returned `showDialog` callback, those + * callback-provided `options` merge over these hook-provided `options` + * + * 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 updating this parameter will not cause a new + * callback to be returned. However, because of the nature of calling dialogs, this has no adverse + * effect on the functionality of this hook. Calling the callback will always use the latest + * `options`. + * @param resolveCallback `(response, dialogType, options)` The function that will be called if the + * dialog request resolves properly + * + * - `response` - the resolved value of the dialog call. Either the user's response or `undefined` if + * the user cancels + * - `dialogType` - the value of `dialogType` at the time that this dialog was called + * - `options` the `options` provided to the dialog at the time that this dialog was called. This + * consists of the `options` provided to the returned `showDialog` callback merged over the + * `options` provided to the hook and additionally contains {@link UseDialogCallbackOptions} + * properties + * + * 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 updating this parameter will not cause a new + * callback to be returned. However, because of the nature of calling dialogs, this has no adverse + * effect on the functionality of this hook. When the dialog resolves, it will always call the + * latest `resolveCallback`. + * @param rejectCallback `(error, dialogType, options)` The function that will be called if the + * dialog request throws an error + * + * - `error` - the error thrown while calling the dialog + * - `dialogType` - the value of `dialogType` at the time that this dialog was called + * - `options` the `options` provided to the dialog at the time that this dialog was called. This + * consists of the `options` provided to the returned `showDialog` callback merged over the + * `options` provided to the hook and additionally contains {@link UseDialogCallbackOptions} + * properties + * + * 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 updating this parameter will not cause a new + * callback to be returned. However, because of the nature of calling dialogs, this has no adverse + * effect on the functionality of this hook. If the dialog throws an error, it will always call + * the latest `rejectCallback`. + * @returns `showDialog(options?)` - callback to run to show the dialog to prompt the user for a + * response + * + * - `optionsOverrides?` - `options` object you may specify that will merge over the `options` you + * provide to the hook before passing to the dialog. All properties are optional, so you may + * specify as many or as few properties here as you want to overwrite the properties in the + * `options` you provide to the hook + */ + function useDialogCallback< + DialogTabType extends DialogTabTypes, + DialogOptions extends DialogTypes[DialogTabType]['options'], + >( + dialogType: DialogTabType, + options: DialogOptions & UseDialogCallbackOptions, + resolveCallback: ( + response: DialogTypes[DialogTabType]['responseType'] | undefined, + dialogType: DialogTabType, + options: DialogOptions, + ) => void, + rejectCallback: (error: unknown, dialogType: DialogTabType, options: DialogOptions) => void, + ): (optionOverrides?: Partial) => Promise; + /** + * + * Enables using `papi.dialogs.showDialog` in React more easily. Returns a callback to run that will + * open a dialog with the provided `dialogType` and `options` then run the `resolveCallback` with + * the dialog response or `rejectCallback` if there is an error. By default, only one dialog can be + * open at a time. + * + * If you need to open multiple dialogs and track which dialog is which, you can set + * `options.shouldOpenMultipleDialogs` to `true` and add a counter to the `options` when calling the + * callback. Then `resolveCallback` will be resolved with that options object including your + * counter. + * + * @type `DialogTabType` The dialog type you are using. Should be inferred by parameters + * @param dialogType Dialog type you want to show on the screen + * + * 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 updating this parameter will not cause a new + * callback to be returned. However, because of the nature of calling dialogs, this has no adverse + * effect on the functionality of this hook. Calling the callback will always use the latest + * `dialogType`. + * @param options Various options for configuring the dialog that shows and this hook. If an + * `options` parameter is also provided to the returned `showDialog` callback, those + * callback-provided `options` merge over these hook-provided `options` + * + * 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 updating this parameter will not cause a new + * callback to be returned. However, because of the nature of calling dialogs, this has no adverse + * effect on the functionality of this hook. Calling the callback will always use the latest + * `options`. + * @param resolveCallback `(response, dialogType, options)` The function that will be called if the + * dialog request resolves properly + * + * - `response` - the resolved value of the dialog call. Either the user's response or `undefined` if + * the user cancels + * - `dialogType` - the value of `dialogType` at the time that this dialog was called + * - `options` the `options` provided to the dialog at the time that this dialog was called. This + * consists of the `options` provided to the returned `showDialog` callback merged over the + * `options` provided to the hook and additionally contains {@link UseDialogCallbackOptions} + * properties + * + * 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 updating this parameter will not cause a new + * callback to be returned. However, because of the nature of calling dialogs, this has no adverse + * effect on the functionality of this hook. When the dialog resolves, it will always call the + * latest `resolveCallback`. + * @param rejectCallback `(error, dialogType, options)` The function that will be called if the + * dialog request throws an error + * + * - `error` - the error thrown while calling the dialog + * - `dialogType` - the value of `dialogType` at the time that this dialog was called + * - `options` the `options` provided to the dialog at the time that this dialog was called. This + * consists of the `options` provided to the returned `showDialog` callback merged over the + * `options` provided to the hook and additionally contains {@link UseDialogCallbackOptions} + * properties + * + * 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 updating this parameter will not cause a new + * callback to be returned. However, because of the nature of calling dialogs, this has no adverse + * effect on the functionality of this hook. If the dialog throws an error, it will always call + * the latest `rejectCallback`. + * @returns `showDialog(options?)` - callback to run to show the dialog to prompt the user for a + * response + * + * - `optionsOverrides?` - `options` object you may specify that will merge over the `options` you + * provide to the hook before passing to the dialog. All properties are optional, so you may + * specify as many or as few properties here as you want to overwrite the properties in the + * `options` you provide to the hook + */ + function useDialogCallback< + DialogTabType extends DialogTabTypes, + DialogOptions extends DialogTypes[DialogTabType]['options'], + >( + dialogType: DialogTabType, + options: DialogOptions & UseDialogCallbackOptions, + resolveCallback: ( + response: DialogTypes[DialogTabType]['responseType'] | undefined, + dialogType: DialogTabType, + options: DialogOptions, + ) => void, + ): (optionOverrides?: Partial) => Promise; + export default useDialogCallback; +} +declare module '@papi/core' { + /** Exporting empty object so people don't have to put 'type' in their import statements */ + const core: {}; + export default core; + export type { ExecutionActivationContext } from 'extension-host/extension-types/extension-activation-context.model'; + export type { ExecutionToken } from 'node/models/execution-token.model'; + export type { DialogTypes } from 'renderer/components/dialogs/dialog-definition.model'; + export type { UseDialogCallbackOptions } from 'renderer/hooks/papi-hooks/use-dialog-callback.hook'; + export type { default as IDataProvider } from 'shared/models/data-provider.interface'; + export type { + DataProviderUpdateInstructions, + DataProviderDataType, + DataProviderSubscriberOptions, + } from 'shared/models/data-provider.model'; + export type { WithNotifyUpdate } from 'shared/models/data-provider-engine.model'; + export type { default as IDataProviderEngine } from 'shared/models/data-provider-engine.model'; + export type { DialogOptions } from 'shared/models/dialog-options.model'; + export type { + ExtensionDataScope, + MandatoryProjectDataType, + } from 'shared/models/project-data-provider.model'; + export type { ProjectMetadata } from 'shared/models/project-metadata.model'; + export type { + GetWebViewOptions, + SavedWebViewDefinition, + UseWebViewStateHook, + WebViewContentType, + WebViewDefinition, + WebViewProps, + } from 'shared/models/web-view.model'; + export type { IWebViewProvider } from 'shared/models/web-view-provider.model'; +} +declare module 'shared/services/menu-data.service-model' { + import { + OnDidDispose, + UnsubscriberAsync, + MultiColumnMenu, + ReferencedItem, + WebViewMenu, + } from 'platform-bible-utils'; + import { + DataProviderDataType, + DataProviderSubscriberOptions, + DataProviderUpdateInstructions, + } from 'shared/models/data-provider.model'; + import { IDataProvider } from '@papi/core'; + /** + * + * This name is used to register the menu data data provider on the papi. You can use this name to + * find the data provider when accessing it using the useData hook + */ + export const menuDataServiceProviderName = 'platform.menuDataServiceDataProvider'; + export const menuDataServiceObjectToProxy: Readonly<{ + /** + * + * This name is used to register the menu data data provider on the papi. You can use this name to + * find the data provider when accessing it using the useData hook + */ + dataProviderName: 'platform.menuDataServiceDataProvider'; + }>; + export type MenuDataDataTypes = { + MainMenu: DataProviderDataType; + WebViewMenu: DataProviderDataType; + }; + module 'papi-shared-types' { + interface DataProviders { + [menuDataServiceProviderName]: IMenuDataService; } - type SettingNames = keyof SettingTypes; - /** This is just a simple example so we have more than one. It's not intended to be real. */ - type NotesOnlyProjectDataTypes = MandatoryProjectDataType & { - Notes: DataProviderDataType; - }; + } + /** + * + * Service that allows to get and store menu data + */ + export type IMenuDataService = { /** - * `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. * - * 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. + * Get menu content for the main menu * - * 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): - * - * @example - * - * ```typescript - * declare module 'papi-shared-types' { - * export type MyProjectDataType = MandatoryProjectDataType & { - * MyProjectData: DataProviderDataType; - * }; - * - * export interface ProjectDataProviders { - * MyExtensionProjectTypeName: IDataProvider; - * } - * } - * ``` + * @param mainMenuType Does not have to be defined + * @returns MultiColumnMenu object of main menu content */ - interface ProjectDataProviders { - 'platform.notesOnly': IDataProvider; - 'platform.placeholder': IDataProvider; - } + getMainMenu(mainMenuType: undefined): Promise; /** - * Names for each project data provider available on the papi. * - * Automatically includes all extensions' project data providers that are added to - * {@link ProjectDataProviders}. + * Get menu content for the main menu * - * @example 'platform.placeholder' + * @param mainMenuType Does not have to be defined + * @returns MultiColumnMenu object of main menu content */ - type ProjectTypes = keyof ProjectDataProviders; + getMainMenu(): Promise; /** - * `DataProviderDataTypes` for each project data provider supported by PAPI. These are the data - * types served by each project data provider. - * - * Automatically includes all extensions' project data providers that are added to - * {@link ProjectDataProviders}. - * - * @example + * This data cannot be changed. Trying to use this setter this will always throw * - * ```typescript - * ProjectDataTypes['MyExtensionProjectTypeName'] => { - * MyProjectData: DataProviderDataType; - * } - * ``` + * @param mainMenuType Does not have to be defined + * @param value MultiColumnMenu object to set as the main menu + * @returns Unsubscriber function */ - type ProjectDataTypes = { - [ProjectType in ProjectTypes]: ExtractDataProviderDataTypes; - }; - type StuffDataTypes = { - Stuff: DataProviderDataType; - }; - type PlaceholderDataTypes = { - Placeholder: DataProviderDataType<{ - thing: number; - }, string[], number>; - }; + setMainMenu( + mainMenuType: undefined, + value: never, + ): Promise>; /** - * `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 - * 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 data provider it registers by - * adding the following to its `.d.ts` file (in this example, we are adding the - * `'helloSomeone.people'` data provider types): + * Subscribe to run a callback function when the main menu data is changed * - * @example - * - * ```typescript - * declare module 'papi-shared-types' { - * export type PeopleDataTypes = { - * Greeting: DataProviderDataType; - * Age: DataProviderDataType; - * People: DataProviderDataType; - * }; - * - * export type PeopleDataMethods = { - * deletePerson(name: string): Promise; - * testRandomMethod(things: string): Promise; - * }; - * - * export type PeopleDataProvider = IDataProvider & PeopleDataMethods; - * - * export interface DataProviders { - * 'helloSomeone.people': PeopleDataProvider; - * } - * } - * ``` + * @param mainMenuType Does not have to be defined + * @param callback Function to run with the updated menuContent for this selector + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber function (run to unsubscribe from listening for updates) */ - interface DataProviders { - 'platform.stuff': IDataProvider; - 'platform.placeholder': IDataProvider; - } + subscribeMainMenu( + mainMenuType: undefined, + callback: (menuContent: MultiColumnMenu) => void, + options?: DataProviderSubscriberOptions, + ): Promise; /** - * Names for each data provider available on the papi. - * - * Automatically includes all extensions' data providers that are added to {@link DataProviders}. + * Get menu content for a web view * - * @example 'platform.placeholder' + * @param webViewType The type of webview for which a menu should be retrieved + * @returns WebViewMenu object of web view menu content */ - type DataProviderNames = keyof DataProviders; + getWebViewMenu(webViewType: ReferencedItem): Promise; /** - * `DataProviderDataTypes` for each data provider supported by PAPI. These are the data types - * served by each data provider. - * - * Automatically includes all extensions' data providers that are added to {@link DataProviders}. + * This data cannot be changed. Trying to use this setter this will always throw * - * @example - * - * ```typescript - * DataProviderTypes['helloSomeone.people'] => { - * Greeting: DataProviderDataType; - * Age: DataProviderDataType; - * People: DataProviderDataType; - * } - * ``` + * @param webViewType The type of webview for which a menu should be set + * @param value Menu of specified webViewType + * @returns Unsubscriber function */ - type DataProviderTypes = { - [DataProviderName in DataProviderNames]: ExtractDataProviderDataTypes; - }; + setWebViewMenu( + webViewType: ReferencedItem, + value: never, + ): Promise>; /** - * Disposable version of each data provider type supported by PAPI. These objects are only - * returned from `papi.dataProviders.registerEngine` - only the one who registers a data provider - * engine is allowed to dispose of the data provider. + * Subscribe to run a callback function when the web view menu data is changed * - * Automatically includes all extensions' data providers that are added to {@link DataProviders}. - */ - type DisposableDataProviders = { - [DataProviderName in DataProviderNames]: IDisposableDataProvider; - }; + * @param webViewType The type of webview for which a menu should be subscribed + * @param callback Function to run with the updated menuContent for this selector + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber function (run to unsubscribe from listening for updates) + */ + subscribeWebViewMenu( + webViewType: ReferencedItem, + callback: (menuContent: WebViewMenu) => void, + options?: DataProviderSubscriberOptions, + ): Promise; + } & OnDidDispose & + typeof menuDataServiceObjectToProxy & + IDataProvider; } -declare module "shared/services/command.service" { - import { UnsubscriberAsync } from 'platform-bible-utils'; - import { CommandHandlers, CommandNames } from 'papi-shared-types'; - module 'papi-shared-types' { - interface CommandHandlers { - 'test.addThree': typeof addThree; - 'test.squareAndConcat': typeof squareAndConcat; - } - } - function addThree(a: number, b: number, c: number): Promise; - function squareAndConcat(a: number, b: string): Promise; - /** Sets up the CommandService. Only runs once and always returns the same promise after that */ - export const initialize: () => Promise; - /** Send a command to the backend. */ - export const sendCommand: (commandName: CommandName, ...args: Parameters) => Promise>>; - /** - * Creates a function that is a command function with a baked commandName. This is also nice because - * you get TypeScript type support using this function. - * - * @param commandName Command name for command function - * @returns Function to call with arguments of command that sends the command and resolves with the - * result of the command - */ - export const createSendCommandFunction: (commandName: CommandName) => (...args: Parameters) => Promise>>; +declare module 'shared/services/menu-data.service' { + import { IMenuDataService } from 'shared/services/menu-data.service-model'; + const menuDataService: IMenuDataService; + export default menuDataService; +} +declare module '@papi/backend' { + /** + * Unified module for accessing API features in the extension host. + * + * WARNING: DO NOT IMPORT papi IN ANY FILE THAT papi IMPORTS AND EXPOSES. + */ + import * as commandService from 'shared/services/command.service'; + import { PapiNetworkService } from 'shared/services/network.service'; + 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 { 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'; + const papi: { /** - * Register a command on the papi to be handled here * - * @param commandName Command name to register for handling here - * - * - Note: Command 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. + * 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`. * - * @param handler Function to run when the command is invoked - * @returns True if successfully registered, throws with error message if not + * @see IDataProviderEngine for more information on extending this class. */ - export const registerCommand: (commandName: CommandName, handler: CommandHandlers[CommandName]) => Promise; + DataProviderEngine: typeof PapiDataProviderEngine; + /** This is just an alias for internet.fetch */ + fetch: typeof globalThis.fetch; /** - * JSDOC SOURCE commandService * * The command service allows you to exchange messages with other components in the platform. You * can register a command that other services and extensions can send you. You can send commands to * other services and extensions that have registered commands. */ - export type moduleSummaryComments = {}; -} -declare module "shared/models/docking-framework.model" { - import { MutableRefObject, ReactNode } from 'react'; - import { DockLayout, DropDirection, LayoutBase } from 'rc-dock'; - import { SavedWebViewDefinition, WebViewDefinition, WebViewDefinitionUpdateInfo } from "shared/models/web-view.model"; - /** - * Saved information used to recreate a tab. - * - * - {@link TabLoader} loads this into {@link TabInfo} - * - {@link TabSaver} saves {@link TabInfo} into this - */ - export type SavedTabInfo = { - /** - * Tab ID - a unique identifier that identifies this tab. If this tab is a WebView, this ID will - * match the `WebViewDefinition.id` - */ - id: string; - /** Type of tab - indicates what kind of built-in tab this info represents */ - tabType: string; - /** Data needed to load the tab */ - data?: unknown; - }; - /** - * Information that Paranext uses to create a tab in the dock layout. - * - * - {@link TabLoader} loads {@link SavedTabInfo} into this - * - {@link TabSaver} saves this into {@link SavedTabInfo} - */ - export type TabInfo = SavedTabInfo & { - /** - * Url of image to show on the title bar of the tab - * - * Defaults to Platform.Bible logo - */ - tabIconUrl?: string; - /** Text to show on the title bar of the tab */ - tabTitle: string; - /** Text to show when hovering over the title bar of the tab */ - tabTooltip?: string; - /** Content to show inside the tab. */ - content: ReactNode; - /** (optional) Minimum width that the tab can become in CSS `px` units */ - minWidth?: number; - /** (optional) Minimum height that the tab can become in CSS `px` units */ - minHeight?: number; - }; - /** - * Function that takes a {@link SavedTabInfo} and creates a Paranext tab out of it. Each type of tab - * must provide a {@link TabLoader}. - * - * For now all tab creators must do their own data type verification - */ - export type TabLoader = (savedTabInfo: SavedTabInfo) => TabInfo; - /** - * Function that takes a Paranext tab and creates a saved tab out of it. Each type of tab can - * provide a {@link TabSaver}. If they do not provide one, the properties added by `TabInfo` are - * stripped from TabInfo by `saveTabInfoBase` before saving (so it is just a {@link SavedTabInfo}). - * - * @param tabInfo The Paranext tab to save - * @returns The saved tab info for Paranext to persist. If `undefined`, does not save the tab - */ - export type TabSaver = (tabInfo: TabInfo) => SavedTabInfo | undefined; - /** Information about a tab in a panel */ - interface TabLayout { - type: 'tab'; - } - /** - * Indicates where to display a floating window - * - * - `cascade` - place the window a bit below and to the right of the previously created floating - * window - * - `center` - center the window in the dock layout - */ - type FloatPosition = 'cascade' | 'center'; - /** The dimensions for a floating tab in CSS `px` units */ - export type FloatSize = { - width: number; - height: number; - }; - /** Information about a floating window */ - export interface FloatLayout { - type: 'float'; - floatSize?: FloatSize; - /** Where to display the floating window. Defaults to `cascade` */ - position?: FloatPosition; - } - export type PanelDirection = 'left' | 'right' | 'bottom' | 'top' | 'before-tab' | 'after-tab' | 'maximize' | 'move' | 'active' | 'update'; - /** Information about a panel */ - interface PanelLayout { - type: 'panel'; - direction?: PanelDirection; - /** If undefined, it will add in the `direction` relative to the previously added tab. */ - targetTabId?: string; - } - /** Information about how a Paranext tab fits into the dock layout */ - export type Layout = TabLayout | FloatLayout | PanelLayout; - /** Event emitted when webViews are created */ - export type AddWebViewEvent = { - webView: SavedWebViewDefinition; - layout: Layout; - }; - /** Props that are passed to the web view tab component */ - export type WebViewTabProps = WebViewDefinition; - /** Rc-dock's onLayoutChange prop made asynchronous - resolves */ - export type OnLayoutChangeRCDock = (newLayout: LayoutBase, currentTabId?: string, direction?: DropDirection) => Promise; - /** Properties related to the dock layout */ - export type PapiDockLayout = { - /** The rc-dock dock layout React element ref. Used to perform operations on the layout */ - dockLayout: DockLayout; - /** - * A ref to a function that runs when the layout changes. We set this ref to our - * {@link onLayoutChange} function - */ - onLayoutChangeRef: MutableRefObject; - /** - * Add or update a tab in the layout - * - * @param savedTabInfo Info for tab to add or update - * @param layout Information about where to put a new tab - * @returns If tab added, final layout used to display the new tab. If existing tab updated, - * `undefined` - */ - addTabToDock: (savedTabInfo: SavedTabInfo, layout: Layout) => Layout | undefined; - /** - * Add or update a webview in the layout - * - * @param webView Web view to add or update - * @param layout Information about where to put a new webview - * @returns If WebView added, final layout used to display the new webView. If existing webView - * updated, `undefined` - */ - addWebViewToDock: (webView: WebViewTabProps, layout: Layout) => Layout | undefined; - /** - * Remove a tab in the layout - * - * @param tabId ID of the tab to remove - */ - removeTabFromDock: (tabId: string) => boolean; - /** - * Gets the WebView definition for the web view with the specified ID - * - * @param webViewId The ID of the WebView whose web view definition to get - * @returns WebView definition with the specified ID or undefined if not found - */ - getWebViewDefinition: (webViewId: string) => WebViewDefinition | undefined; - /** - * Updates the WebView with the specified ID with the specified properties - * - * @param webViewId The ID of the WebView to update - * @param updateInfo Properties to update on the WebView. Any unspecified properties will stay the - * same - * @returns True if successfully found the WebView to update; false otherwise - */ - updateWebViewDefinition: (webViewId: string, updateInfo: WebViewDefinitionUpdateInfo) => boolean; - /** - * The layout to use as the default layout if the dockLayout doesn't have a layout loaded. - * - * TODO: This should be removed and the `testLayout` imported directly in this file once this - * service is refactored to split the code between processes. The only reason this is passed from - * `platform-dock-layout.component.tsx` is that we cannot import `testLayout` here since this - * service is currently all shared code. Refactor should happen in #203 - */ - testLayout: LayoutBase; - }; -} -declare module "shared/services/web-view.service-model" { - import { GetWebViewOptions, WebViewId, WebViewType } from "shared/models/web-view.model"; - import { AddWebViewEvent, Layout } from "shared/models/docking-framework.model"; - import { PlatformEvent } from 'platform-bible-utils'; + commands: typeof commandService; /** - * JSDOC SOURCE papiWebViewService * * Service exposing various functions related to using webViews * * WebViews are iframes in the Platform.Bible UI into which extensions load frontend code, either * HTML or React components. */ - export interface WebViewServiceType { - /** Event that emits with webView info when a webView is added */ - onDidAddWebView: PlatformEvent; - /** - * Creates a new web view or gets an existing one depending on if you request an existing one and - * if the web view provider decides to give that existing one to you (it is up to the provider). - * - * @param webViewType Type of WebView to create - * @param layout Information about where you want the web view to go. Defaults to adding as a tab - * @param options Options that affect what this function does. For example, you can provide an - * existing web view ID to request an existing web view with that ID. - * @returns Promise that resolves to the ID of the webview we got or undefined if the provider did - * not create a WebView for this request. - * @throws If something went wrong like the provider for the webViewType was not found - */ - getWebView: (webViewType: WebViewType, layout?: Layout, options?: GetWebViewOptions) => Promise; - } - /** Name to use when creating a network event that is fired when webViews are created */ - export const EVENT_NAME_ON_DID_ADD_WEB_VIEW: `${string}:${string}`; - export const NETWORK_OBJECT_NAME_WEB_VIEW_SERVICE = "WebViewService"; -} -declare module "shared/services/network-object-status.service-model" { - import { NetworkObjectDetails } from "shared/models/network-object.model"; - export interface NetworkObjectStatusRemoteServiceType { - /** - * Get details about all available network objects - * - * @returns Object whose keys are the names of the network objects and whose values are the - * {@link NetworkObjectDetails} for each network object - */ - getAllNetworkObjectDetails: () => Promise>; - } - /** Provides functions related to the set of available network objects */ - export interface NetworkObjectStatusServiceType extends NetworkObjectStatusRemoteServiceType { - /** - * Get a promise that resolves when a network object is registered or rejects if a timeout is hit - * - * @returns Promise that either resolves to the {@link NetworkObjectDetails} for a network object - * once the network object is registered, or rejects if a timeout is provided and the timeout is - * reached before the network object is registered - */ - waitForNetworkObject: (id: string, timeoutInMS?: number) => Promise; - } - export const networkObjectStatusServiceNetworkObjectName = "NetworkObjectStatusService"; -} -declare module "shared/services/network-object-status.service" { - import { NetworkObjectStatusServiceType } from "shared/services/network-object-status.service-model"; - const networkObjectStatusService: NetworkObjectStatusServiceType; - export default networkObjectStatusService; -} -declare module "shared/services/web-view.service" { - import { WebViewServiceType } from "shared/services/web-view.service-model"; - const webViewService: WebViewServiceType; - export default webViewService; -} -declare module "shared/models/web-view-provider.model" { - import { GetWebViewOptions, WebViewDefinition, SavedWebViewDefinition } from "shared/models/web-view.model"; - import { DisposableNetworkObject, NetworkObject, NetworkableObject } from "shared/models/network-object.model"; - import { CanHaveOnDidDispose } from 'platform-bible-utils'; - export interface IWebViewProvider extends NetworkableObject { - /** - * @param savedWebView Filled out if an existing webview is being called for (matched by ID). Just - * ID if this is a new request or if the web view with the existing ID was not found - * @param getWebViewOptions - */ - getWebView(savedWebView: SavedWebViewDefinition, getWebViewOptions: GetWebViewOptions): Promise; - } - export interface WebViewProvider extends NetworkObject, CanHaveOnDidDispose { - } - export interface DisposableWebViewProvider extends DisposableNetworkObject, Omit { - } -} -declare module "shared/services/web-view-provider.service" { - /** - * Handles registering web view providers and serving web views around the papi. Exposed on the - * papi. - */ - import { DisposableWebViewProvider, IWebViewProvider, WebViewProvider } from "shared/models/web-view-provider.model"; - /** Sets up the service. Only runs once and always returns the same promise after that */ - const initialize: () => Promise; + webViews: WebViewServiceType; /** - * Indicate if we are aware of an existing web view provider with the given type. If a web view - * provider with the given type 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. - * - * @param webViewType Type of webView to check for - */ - function hasKnown(webViewType: string): boolean; - /** - * Register a web view provider to serve webViews for a specified type of webViews - * - * @param webViewType Type of web view to provide - * @param webViewProvider Object to register as a webView provider including control over disposing - * of it. - * - * WARNING: setting a webView provider mutates the provided object. - * @returns `webViewProvider` modified to be a network object - */ - function register(webViewType: string, webViewProvider: IWebViewProvider): Promise; - /** - * Get a web view provider that has previously been set up - * - * @param webViewType Type of webview provider to get - * @returns Web view provider with the given name if one exists, undefined otherwise - */ - function get(webViewType: string): Promise; - export interface WebViewProviderService { - initialize: typeof initialize; - hasKnown: typeof hasKnown; - register: typeof register; - get: typeof get; - } - export interface PapiWebViewProviderService { - register: typeof register; - } - const webViewProviderService: WebViewProviderService; - /** - * JSDOC SOURCE papiWebViewProviderService * * Interface for registering webView providers */ - export const papiWebViewProviderService: PapiWebViewProviderService; - export default webViewProviderService; -} -declare module "shared/services/internet.service" { - /** Our shim over fetch. Allows us to control internet access. */ - const papiFetch: typeof fetch; - export interface InternetService { - fetch: typeof papiFetch; - } - /** - * JSDOC SOURCE internetService - * - * Service that provides a way to call `fetch` since the original function is not available - */ - const internetService: InternetService; - export default internetService; -} -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 } from "shared/models/data-provider-engine.model"; - import { DataProviderNames, DataProviderTypes, DataProviders, DisposableDataProviders } from 'papi-shared-types'; - import IDataProvider, { IDisposableDataProvider } from "shared/models/data-provider.interface"; - /** - * 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 { - 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 - * - * @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 - */ - function ignore(method: Function & { - isIgnored?: boolean; - }): void; + webViewProviders: PapiWebViewProviderService; /** - * 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 - * - * @param target The class that has the method to ignore - * @param member The name of the method to ignore * - * 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. + * Prompt the user for responses with dialogs */ - function ignore(target: T, member: keyof T): void; + dialogs: DialogService; /** - * 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. + * Service that provides a way to send and receive network events */ - const decorators: { - ignore: typeof ignore; - }; + network: PapiNetworkService; /** - * 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. - */ - function registerEngine(providerName: DataProviderName, dataProviderEngine: IDataProviderEngine, dataProviderType?: string, dataProviderAttributes?: { - [property: string]: unknown; - } | undefined): Promise; - /** - * Creates a data provider to be shared on the network layering over the provided data provider - * engine. - * - * @type `TDataTypes` - The data provider data types served by the data provider to create. - * - * This is not exposed on the papi as it is a helper function to enable other services to layer over - * this service and create their own subsets of data providers with other types than - * `DataProviders` types using this function and {@link getByType} - * @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. - */ - export function registerEngineByType(providerName: string, dataProviderEngine: IDataProviderEngine, dataProviderType?: string, dataProviderAttributes?: { - [property: string]: unknown; - } | undefined): Promise>>; - /** - * 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 - */ - function get(providerName: DataProviderName): Promise; - /** - * Get a data provider that has previously been set up - * - * @type `T` - The type of data provider to get. Use `IDataProvider`, - * specifying your own types, or provide a custom data provider type - * - * This is not exposed on the papi as it is a helper function to enable other services to layer over - * this service and create their own subsets of data providers with other types than - * `DataProviders` types using this function and {@link registerEngineByType} - * @param providerName Name of the desired data provider - * @returns The data provider with the given name if one exists, undefined otherwise - */ - export function getByType>(providerName: string): Promise; - export interface DataProviderService { - hasKnown: typeof hasKnown; - registerEngine: typeof registerEngine; - get: typeof get; - decorators: typeof decorators; - DataProviderEngine: typeof DataProviderEngine; - } - /** - * JSDOC SOURCE dataProviderService * - * Service that allows extensions to send and receive data to/from other extensions + * All extensions and services should use this logger to provide a unified output of logs */ - const dataProviderService: DataProviderService; - 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 */ - export type ProjectDataProviderEngineTypes = { - [ProjectType in ProjectTypes]: IDataProviderEngine; + logger: import('electron-log').MainLogger & { + default: import('electron-log').MainLogger; }; - export interface ProjectDataProviderEngineFactory { - createProjectDataProviderEngine(projectId: string, projectStorageInterpreterId: string): ProjectDataProviderEngineTypes[ProjectType]; - } -} -declare module "shared/models/project-metadata.model" { - import { ProjectTypes } from 'papi-shared-types'; - /** - * Low-level information describing a project that Platform.Bible directly manages and uses to load - * project data - */ - export type ProjectMetadata = { - /** ID of the project (must be unique and case insensitive) */ - id: string; - /** Short name of the project (not necessarily unique) */ - name: string; - /** Indicates how the project is persisted to storage */ - storageType: string; - /** - * Indicates what sort of project this is which implies its data shape (e.g., what data streams - * should be available) - */ - projectType: ProjectTypes; - }; -} -declare module "shared/services/project-lookup.service-model" { - import { ProjectMetadata } from "shared/models/project-metadata.model"; /** - * JSDOC SOURCE projectLookupService * - * Provides metadata for projects known by the platform + * Service that provides a way to call `fetch` since the original function is not available */ - export interface ProjectLookupServiceType { - /** - * Provide metadata for all projects found on the local system - * - * @returns ProjectMetadata for all projects stored on the local system - */ - getMetadataForAllProjects: () => Promise; - /** - * Look up metadata for a specific project ID - * - * @param projectId ID of the project to load - * @returns ProjectMetadata from the 'meta.json' file for the given project - */ - getMetadataForProject: (projectId: string) => Promise; - } - export const projectLookupServiceNetworkObjectName = "ProjectLookupService"; -} -declare module "shared/services/project-lookup.service" { - import { ProjectLookupServiceType } from "shared/services/project-lookup.service-model"; - const projectLookupService: ProjectLookupServiceType; - export default projectLookupService; -} -declare module "shared/services/project-data-provider.service" { - import { ProjectTypes, ProjectDataProviders } from 'papi-shared-types'; - import { ProjectDataProviderEngineFactory } from "shared/models/project-data-provider-engine.model"; - import { Dispose } from 'platform-bible-utils'; + internet: InternetService; /** - * Add a new Project Data Provider Factory to PAPI that uses the given engine. There must not be an - * existing factory already that handles the same project type or this operation will fail. * - * @param projectType Type of project that pdpEngineFactory supports - * @param pdpEngineFactory Used in a ProjectDataProviderFactory to create ProjectDataProviders - * @returns Promise that resolves to a disposable object when the registration operation completes + * Service that allows extensions to send and receive data to/from other extensions */ - export function registerProjectDataProviderEngineFactory(projectType: ProjectType, pdpEngineFactory: ProjectDataProviderEngineFactory): Promise; - /** - * Get a Project Data Provider for the given project ID. - * - * @example - * - * ```typescript - * const pdp = await get('ParatextStandard', 'ProjectID12345'); - * pdp.getVerse(new VerseRef('JHN', '1', '1')); - * ``` - * - * @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 - * project's actual `projectType` (according to its metadata), a warning will be logged - * @param projectId ID for the project to load - * @returns Data provider with types that are associated with the given project type - */ - export function get(projectType: ProjectType, projectId: string): Promise; - export interface PapiBackendProjectDataProviderService { - registerProjectDataProviderEngineFactory: typeof registerProjectDataProviderEngineFactory; - get: typeof get; - } + dataProviders: DataProviderService; /** - * JSDOC SOURCE papiBackendProjectDataProviderService * * Service that registers and gets project data providers */ - export const papiBackendProjectDataProviderService: PapiBackendProjectDataProviderService; - export interface PapiFrontendProjectDataProviderService { - get: typeof get; - } + projectDataProviders: PapiBackendProjectDataProviderService; /** - * JSDOC SOURCE papiFrontendProjectDataProviderService * - * Service that gets project data providers + * Provides metadata for projects known by the platform */ - export const papiFrontendProjectDataProviderService: { - get: typeof get; - }; -} -declare module "shared/data/file-system.model" { - /** Types to use with file system operations */ + projectLookup: ProjectLookupServiceType; /** - * Represents a path in file system or other. Has a scheme followed by :// followed by a relative - * path. If no scheme is provided, the app scheme is used. Available schemes are as follows: * - * - `app://` - goes to the app's home directory and into `.platform.bible` (platform-dependent) - * - `cache://` - goes to the app's temporary file cache at `app://cache` - * - `data://` - goes to the app's data storage location at `app://data` - * - `resources://` - goes to the resources directory installed in the app - * - `file://` - an absolute file path from root + * This service provides extensions in the extension host the ability to read/write data based on + * the extension identity and current user (as identified by the OS). This service will not work + * within the renderer. */ - export type Uri = string; -} -declare module "node/utils/util" { - import { Uri } from "shared/data/file-system.model"; - export const FILE_PROTOCOL = "file://"; - export const RESOURCES_PROTOCOL = "resources://"; - export function resolveHtmlPath(htmlFileName: string): string; + storage: ExtensionStorageService; /** - * Gets the platform-specific user Platform.Bible folder for this application - * - * When running in development: `/dev-appdata` * - * When packaged: `/.platform.bible` + * Service that allows to get and store menu data */ - export const getAppDir: import("memoize-one").MemoizedFn<() => string>; + menuData: IMenuDataService; + }; + export default papi; + /** + * + * 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 const DataProviderEngine: typeof PapiDataProviderEngine; + /** This is just an alias for internet.fetch */ + export const fetch: typeof globalThis.fetch; + /** + * + * The command service allows you to exchange messages with other components in the platform. You + * can register a command that other services and extensions can send you. You can send commands to + * other services and extensions that have registered commands. + */ + export const commands: typeof commandService; + /** + * + * Service exposing various functions related to using webViews + * + * WebViews are iframes in the Platform.Bible UI into which extensions load frontend code, either + * HTML or React components. + */ + export const webViews: WebViewServiceType; + /** + * + * Interface for registering webView providers + */ + export const webViewProviders: PapiWebViewProviderService; + /** + * + * Prompt the user for responses with dialogs + */ + export const dialogs: DialogService; + /** + * + * Service that provides a way to send and receive network events + */ + export const network: PapiNetworkService; + /** + * + * All extensions and services should use this logger to provide a unified output of logs + */ + export const logger: import('electron-log').MainLogger & { + default: import('electron-log').MainLogger; + }; + /** + * + * Service that provides a way to call `fetch` since the original function is not available + */ + export const internet: InternetService; + /** + * + * Service that allows extensions to send and receive data to/from other extensions + */ + export const dataProviders: DataProviderService; + /** + * + * Service that registers and gets project data providers + */ + export const projectDataProviders: PapiBackendProjectDataProviderService; + /** + * + * Provides metadata for projects known by the platform + */ + export const projectLookup: ProjectLookupServiceType; + /** + * + * This service provides extensions in the extension host the ability to read/write data based on + * the extension identity and current user (as identified by the OS). This service will not work + * within the renderer. + */ + export const storage: ExtensionStorageService; + /** + * + * Service that allows to get and store menu data + */ + export const menuData: IMenuDataService; +} +declare module 'extension-host/extension-types/extension.interface' { + import { UnsubscriberAsync } from 'platform-bible-utils'; + import { ExecutionActivationContext } from 'extension-host/extension-types/extension-activation-context.model'; + /** Interface for all extensions to implement */ + export interface IExtension { /** - * Resolves the uri to a path + * Sets up this extension! Runs when paranext wants this extension to activate. For example, + * activate() should register commands for this extension * - * @param uri The uri to resolve - * @returns Real path to the uri supplied + * @param context Data and utilities that are specific to this particular extension */ - export function getPathFromUri(uri: Uri): string; + activate: (context: ExecutionActivationContext) => Promise; /** - * Combines the uri passed in with the paths passed in to make one uri + * Deactivate anything in this extension that is not covered by the registrations in the context + * object given to activate(). * - * @param uri Uri to start from - * @param paths Paths to combine into the uri - * @returns One uri that combines the uri and the paths in left-to-right order + * @returns Promise that resolves to true if successfully deactivated */ - export function joinUriPaths(uri: Uri, ...paths: string[]): Uri; + deactivate?: UnsubscriberAsync; + } } -declare module "node/services/node-file-system.service" { - /** File system calls from Node */ - import fs, { BigIntStats } from 'fs'; - import { Uri } from "shared/data/file-system.model"; +declare module 'extension-host/extension-types/extension-manifest.model' { + /** Information about an extension provided by the extension developer. */ + export type ExtensionManifest = { + /** Name of the extension */ + name: string; /** - * Read a text file + * Extension version - expected to be [semver](https://semver.org/) like `"0.1.3"`. * - * @param uri URI of file - * @returns Promise that resolves to the contents of the file + * Note: semver may become a hard requirement in the future, so we recommend using it now. */ - export function readFileText(uri: Uri): Promise; + version: string; /** - * Read a binary file + * Path to the JavaScript file to run in the extension host. Relative to the extension's root + * folder. * - * @param uri URI of file - * @returns Promise that resolves to the contents of the file + * Must be specified. Can be an empty string if the extension does not have any JavaScript to run. */ - export function readFileBinary(uri: Uri): Promise; + main: string; /** - * Write data to a file + * Path to the TypeScript type declaration file that describes this extension and its interactions + * on the PAPI. Relative to the extension's root folder. * - * @param uri URI of file - * @param fileContents String or Buffer to write into the file - * @returns Promise that resolves after writing the file - */ - export function writeFile(uri: Uri, fileContents: string | Buffer): Promise; - /** - * Copies a file from one location to another. Creates the path to the destination if it does not - * exist + * If not provided, Platform.Bible will look in the following locations: * - * @param sourceUri The location of the file to copy - * @param destinationUri The uri to the file to create as a copy of the source file - * @param mode Bitwise modifiers that affect how the copy works. See - * [`fsPromises.copyFile`](https://nodejs.org/api/fs.html#fspromisescopyfilesrc-dest-mode) for - * more information - */ - export function copyFile(sourceUri: Uri, destinationUri: Uri, mode?: Parameters[2]): Promise; - /** - * Delete a file if it exists + * 1. `.d.ts` + * 2. `.d.ts` + * 3. `index.d.ts` * - * @param uri URI of file - * @returns Promise that resolves when the file is deleted or determined to not exist + * See [Extension Anatomy - Type Declaration + * Files](https://github.com/paranext/paranext-extension-template/wiki/Extension-Anatomy#type-declaration-files-dts) + * for more information about extension type declaration files. */ - export function deleteFile(uri: Uri): Promise; + types?: string; /** - * Get stats about the file or directory. Note that BigInts are used instead of ints to avoid. - * https://en.wikipedia.org/wiki/Year_2038_problem - * - * @param uri URI of file or directory - * @returns Promise that resolves to object of type https://nodejs.org/api/fs.html#class-fsstats if - * file or directory exists, undefined if it doesn't - */ - export function getStats(uri: Uri): Promise; - /** - * Set the last modified and accessed times for the file or directory - * - * @param uri URI of file or directory - * @returns Promise that resolves once the touch operation finishes + * List of events that occur that should cause this extension to be activated. Not yet + * implemented. */ - export function touch(uri: Uri, date: Date): Promise; - /** Type of file system item in a directory */ - export enum EntryType { - File = "file", - Directory = "directory", - Unknown = "unknown" + activationEvents: string[]; + }; +} +declare module 'shared/services/settings.service-model' { + import { SettingNames, SettingTypes } from 'papi-shared-types'; + import { OnDidDispose, PlatformEventEmitter, Unsubscriber } from 'platform-bible-utils'; + import { DataProviderUpdateInstructions, IDataProvider } from '@papi/core'; + /** + * + * Name used to register the data provider + * + * You can use this name + */ + export const settingsServiceDataProviderName = 'platform.settingsServiceDataProvider'; + export const settingsServiceObjectToProxy: Readonly<{ + /** + * + * Name used to register the data provider + * + * You can use this name + */ + dataProviderName: 'platform.settingsServiceDataProvider'; + }>; + /** + * SettingDataTypes handles getting and setting Platform.Bible core application and extension + * settings. + * + * Note: the unnamed (`''`) data type is not actually part of `SettingDataTypes` because the methods + * would not be able to create a generic type extending from `SettingNames` in order to return the + * specific setting type being requested. As such, `get`, `set`, `reset` and `subscribe` are all + * specified on {@link ISettingsService} instead. Unfortunately, as a result, using Intellisense with + * `useData` will not show the unnamed data type (`''`) as an option, but you can use `useSetting` + * instead. However, do note that the unnamed data type (`''`) is fully functional. + */ + export type SettingDataTypes = {}; + module 'papi-shared-types' { + interface DataProviders { + [settingsServiceDataProviderName]: ISettingsService; } - /** All entries in a directory, mapped from entry type to array of uris for the entries */ - export type DirectoryEntries = Readonly<{ - [entryType in EntryType]: Uri[]; - }>; - /** - * Reads a directory and returns lists of entries in the directory by entry type. - * - * @param uri - URI of directory. - * @param entryFilter - Function to filter out entries in the directory based on their names. - * @returns Map of entry type to list of uris for each entry in the directory with that type. - */ - export function readDir(uri: Uri, entryFilter?: (entryName: string) => boolean): Promise; - /** - * Create a directory in the file system if it does not exist. Does not throw if it already exists. - * - * @param uri URI of directory - * @returns Promise that resolves once the directory has been created - */ - export function createDir(uri: Uri): Promise; - /** - * Remove a directory and all its contents recursively from the file system - * - * @param uri URI of directory - * @returns Promise that resolves when the delete operation finishes - */ - export function deleteDir(uri: Uri): Promise; + } + /** Event to set or update a setting */ + export type UpdateSettingEvent = { + type: 'update-setting'; + setting: SettingTypes[SettingName]; + }; + /** Event to remove a setting */ + export type ResetSettingEvent = { + type: 'reset-setting'; + }; + /** All supported setting events */ + export type SettingEvent = + | UpdateSettingEvent + | ResetSettingEvent; + /** All message subscriptions - emitters that emit an event each time a setting is updated */ + export const onDidUpdateSettingEmitters: Map< + keyof SettingTypes, + PlatformEventEmitter> + >; + /** */ + export type ISettingsService = { + /** + * Retrieves the value of the specified setting + * + * @param key The string id of the setting for which the value is being retrieved + * @param defaultSetting The default value used for the setting if no value is available for the + * key + * @returns The value of the specified setting, parsed to an object. Returns default setting if + * setting does not exist + */ + get(key: SettingName): Promise; + /** + * Sets the value of the specified setting + * + * @param key The string id of the setting for which the value is being retrieved + * @param newSetting The value that is to be stored. Setting the new value to `undefined` is the + * equivalent of deleting the setting + */ + set( + key: SettingName, + newSetting: SettingTypes[SettingName], + ): Promise>; + /** + * Removes the setting from memory + * + * @param key The string id of the setting for which the value is being removed + * @returns `true` if successfully reset the project setting. `false` otherwise + */ + reset(key: SettingName): Promise; + /** + * Subscribes to updates of the specified setting. Whenever the value of the setting changes, the + * callback function is executed. + * + * @param key The string id of the setting for which the value is being subscribed to + * @param callback The function that will be called whenever the specified setting is updated + * @returns Unsubscriber that should be called whenever the subscription should be deleted + */ + subscribe( + key: SettingName, + callback: (newSetting: SettingEvent) => void, + ): Promise; + } & OnDidDispose & + IDataProvider & + typeof settingsServiceObjectToProxy; } -declare module "node/utils/crypto-util" { - export function createUuid(): string; - /** - * Create a cryptographically secure nonce that is at least 128 bits long. See nonce spec at - * https://w3c.github.io/webappsec-csp/#security-nonces - * - * @param encoding: "base64url" (HTML safe, shorter string) or "hex" (longer string) From - * https://base64.guru/standards/base64url, the purpose of this encoding is "the ability to use - * the encoding result as filename or URL address" - * @param numberOfBytes: Number of bytes the resulting nonce should contain - * @returns Cryptographically secure, pseudo-randomly generated value encoded as a string - */ - export function createNonce(encoding: 'base64url' | 'hex', numberOfBytes?: number): string; +declare module 'shared/services/settings.service' { + import { ISettingsService } from 'shared/services/settings.service-model'; + const settingsService: ISettingsService; + export default settingsService; } -declare module "node/models/execution-token.model" { - /** For now this is just for extensions, but maybe we will want to expand this in the future */ - export type ExecutionTokenType = 'extension'; - /** Execution tokens can be passed into API calls to provide context about their identity */ - export class ExecutionToken { - readonly type: ExecutionTokenType; - readonly name: string; - readonly nonce: string; - constructor(tokenType: ExecutionTokenType, name: string); - getHash(): string; - } +declare module 'renderer/hooks/hook-generators/create-use-network-object-hook.util' { + import { NetworkObject } from 'shared/models/network-object.model'; + /** + * This function takes in a getNetworkObject function and creates a hook with that function in it + * which will return a network object + * + * @param getNetworkObject A function that takes in an id string and returns a network object + * @param mapParametersToNetworkObjectSource Function that takes the parameters passed into the hook + * and returns the `networkObjectSource` associated with those parameters. Defaults to taking the + * first parameter passed into the hook and using that as the `networkObjectSource`. + * + * - Note: `networkObjectSource` is string name of the network object to get OR `networkObject` + * (result of this hook, if you want this hook to just return the network object again) + * + * @returns A function that takes in a networkObjectSource and returns a NetworkObject + */ + function createUseNetworkObjectHook( + getNetworkObject: (...args: THookParams) => Promise | undefined>, + mapParametersToNetworkObjectSource?: ( + ...args: THookParams + ) => string | NetworkObject | undefined, + ): (...args: THookParams) => NetworkObject | undefined; + export default createUseNetworkObjectHook; } -declare module "node/services/execution-token.service" { - import { ExecutionToken } from "node/models/execution-token.model"; - /** - * This should be called when extensions are being loaded - * - * @param extensionName Name of the extension to register - * @returns Token that can be passed to `tokenIsValid` to authenticate or authorize API callers. It - * is important that the token is not shared to avoid impersonation of API callers. - */ - function registerExtension(extensionName: string): ExecutionToken; - /** - * Remove a registered token. Note that a hash of a token is what is needed to unregister, not the - * full token itself (notably not the nonce), so something can be delegated the ability to - * unregister a token without having been given the full token itself. - * - * @param extensionName Name of the extension that was originally registered - * @param tokenHash Value of `getHash()` of the token that was originally registered. - * @returns `true` if the token was successfully unregistered, `false` otherwise - */ - function unregisterExtension(extensionName: string, tokenHash: string): boolean; - /** - * This should only be needed by services that need to contextualize the response for the caller - * - * @param executionToken Token that was previously registered. - * @returns `true` if the token matches a token that was previous registered, `false` otherwise. - */ - function tokenIsValid(executionToken: ExecutionToken): boolean; - const executionTokenService: { - registerExtension: typeof registerExtension; - unregisterExtension: typeof unregisterExtension; - tokenIsValid: typeof tokenIsValid; +declare module 'renderer/hooks/papi-hooks/use-data-provider.hook' { + import { DataProviders } from 'papi-shared-types'; + /** + * Gets a data provider with specified provider name + * + * @type `T` - The type of data provider to return. Use `IDataProvider`, + * specifying your own types, or provide a custom data provider type + * @param dataProviderSource String name of the data provider to get OR dataProvider (result of + * useDataProvider, if you want this hook to just return the data provider again) + * @returns Undefined if the data provider has not been retrieved, data provider if it has been + * retrieved and is not disposed, and undefined again if the data provider is disposed + */ + const useDataProvider: ( + dataProviderSource: DataProviderName | DataProviders[DataProviderName] | undefined, + ) => DataProviders[DataProviderName] | undefined; + export default useDataProvider; +} +declare module 'renderer/hooks/hook-generators/create-use-data-hook.util' { + import { + DataProviderSubscriberOptions, + DataProviderUpdateInstructions, + } from 'shared/models/data-provider.model'; + import IDataProvider from 'shared/models/data-provider.interface'; + import ExtractDataProviderDataTypes from 'shared/models/extract-data-provider-data-types.model'; + /** + * The final function called as part of the `useData` hook that is the actual React hook + * + * This is the `.Greeting(...)` part of `useData('helloSomeone.people').Greeting(...)` + */ + type UseDataFunctionWithProviderType< + TDataProvider extends IDataProvider, + TDataType extends keyof ExtractDataProviderDataTypes, + > = ( + selector: ExtractDataProviderDataTypes[TDataType]['selector'], + defaultValue: ExtractDataProviderDataTypes[TDataType]['getData'], + subscriberOptions?: DataProviderSubscriberOptions, + ) => [ + ExtractDataProviderDataTypes[TDataType]['getData'], + ( + | (( + newData: ExtractDataProviderDataTypes[TDataType]['setData'], + ) => Promise>>) + | undefined + ), + boolean, + ]; + /** + * A proxy that serves the actual hooks for a single data provider + * + * This is the `useData('helloSomeone.people')` part of + * `useData('helloSomeone.people').Greeting(...)` + */ + type UseDataProxy> = { + [TDataType in keyof ExtractDataProviderDataTypes]: UseDataFunctionWithProviderType< + TDataProvider, + TDataType + >; + }; + /** + * React hook to use data provider data with various data types + * + * @example `useData('helloSomeone.people').Greeting('Bill', 'Greeting loading')` + * + * @type `TDataProvider` - The type of data provider to get. Use + * `IDataProvider`, specifying your own types, or provide a custom data + * provider type + */ + type UseDataHookGeneric = { + >( + ...args: TUseDataProviderParams + ): UseDataProxy; + }; + /** + * Create a `useData(...).DataType(selector, defaultValue, options)` hook for a specific subset of + * data providers as supported by `useDataProviderHook` + * + * @param useDataProviderHook Hook that gets a data provider from a specific subset of data + * providers + * @returns `useData` hook for getting data from a data provider + */ + function createUseDataHook( + useDataProviderHook: (...args: TUseDataProviderParams) => IDataProvider | undefined, + ): UseDataHookGeneric; + export default createUseDataHook; +} +declare module 'renderer/hooks/papi-hooks/use-data.hook' { + import { + DataProviderSubscriberOptions, + DataProviderUpdateInstructions, + } from 'shared/models/data-provider.model'; + import { DataProviderNames, DataProviderTypes, DataProviders } from 'papi-shared-types'; + /** + * React hook to use data from a data provider + * + * @example `useData('helloSomeone.people').Greeting('Bill', 'Greeting loading')` + */ + type UseDataHook = { + ( + dataProviderSource: DataProviderName | DataProviders[DataProviderName] | undefined, + ): { + [TDataType in keyof DataProviderTypes[DataProviderName]]: ( + // @ts-ignore TypeScript pretends it can't find `selector`, but it works just fine + selector: DataProviderTypes[DataProviderName][TDataType]['selector'], + // @ts-ignore TypeScript pretends it can't find `getData`, but it works just fine + defaultValue: DataProviderTypes[DataProviderName][TDataType]['getData'], + subscriberOptions?: DataProviderSubscriberOptions, + ) => [ + // @ts-ignore TypeScript pretends it can't find `getData`, but it works just fine + DataProviderTypes[DataProviderName][TDataType]['getData'], + ( + | (( + // @ts-ignore TypeScript pretends it can't find `setData`, but it works just fine + newData: DataProviderTypes[DataProviderName][TDataType]['setData'], + ) => Promise>) + | undefined + ), + boolean, + ]; }; - export default executionTokenService; + }; + /** + * ```typescript + * useData( + * dataProviderSource: DataProviderName | DataProviders[DataProviderName] | undefined, + * ).DataType( + * selector: DataProviderTypes[DataProviderName][DataType]['selector'], + * defaultValue: DataProviderTypes[DataProviderName][DataType]['getData'], + * subscriberOptions?: DataProviderSubscriberOptions, + * ) => [ + * DataProviderTypes[DataProviderName][DataType]['getData'], + * ( + * | (( + * newData: DataProviderTypes[DataProviderName][DataType]['setData'], + * ) => Promise>) + * | undefined + * ), + * boolean, + * ] + * ``` + * + * React hook to use data from a data provider. Subscribes to run a callback on a data provider's + * data with specified selector on the specified data type that data provider serves. + * + * Usage: Specify the data provider and the data type on the data provider with + * `useData('').` and use like any other React hook. + * + * _@example_ Subscribing to Verse data at JHN 11:35 on the `'quickVerse.quickVerse'` data provider: + * + * ```typescript + * const [verseText, setVerseText, verseTextIsLoading] = useData('quickVerse.quickVerse').Verse( + * 'JHN 11:35', + * 'Verse text goes here', + * ); + * ``` + * + * _@param_ `dataProviderSource` string name of data provider to get OR dataProvider (result of + * useDataProvider if you want to consolidate and only get the data provider once) + * + * _@param_ `selector` tells the provider what data this listener is listening for + * + * 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 data + * + * _@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_ `[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 + * - `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 + * 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 + * provider + */ + const useData: UseDataHook; + export default useData; } -declare module "extension-host/services/extension-storage.service" { - import { ExecutionToken } from "node/models/execution-token.model"; - import { Buffer } from 'buffer'; - /** - * This is only intended to be called by the extension service. This service cannot call into the - * extension service or it causes a circular dependency. - */ - export function setExtensionUris(urisPerExtension: Map): void; - /** Return a path to the specified file within the extension's installation directory */ - export function buildExtensionPathFromName(extensionName: string, fileName: string): string; - /** - * Read a text file from the the extension's installation directory +declare module 'renderer/hooks/papi-hooks/use-setting.hook' { + import { SettingTypes } from 'papi-shared-types'; + import { + DataProviderSubscriberOptions, + DataProviderUpdateInstructions, + } from 'shared/models/data-provider.model'; + import { SettingDataTypes } from 'shared/services/settings.service-model'; + /** + * Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes + * and gets updated when the setting is changed by others. + * + * @param key The string id that is used to store the setting in local storage + * + * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be + * updated every render + * @param defaultState The default state of the setting. If the setting already has a value set to + * it in local storage, this parameter will be ignored. + * + * Note: this parameter is internally assigned to a `ref`, so changing it will not cause any hooks + * to re-run with its new value. Running `resetSetting()` will always update the setting value + * returned to the latest `defaultState`, and changing the `key` will use the latest + * `defaultState`. However, if `defaultState` is changed while a setting is `defaultState` + * (meaning it is reset and has no value), the returned setting value will not be updated to the + * new `defaultState`. + * @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 state of the setting, either `defaultState` or the stored state on the + * papi, if any + * - `setSetting`: Function that updates the setting to a new value + * - `resetSetting`: Function that removes the setting and resets the value to `defaultState` + * + * @throws When subscription callback function is called with an update that has an unexpected + * message type + */ + const useSetting: ( + key: SettingName, + defaultState: SettingTypes[SettingName], + subscriberOptions?: DataProviderSubscriberOptions, + ) => [ + setting: SettingTypes[SettingName], + setSetting: ( + newData: SettingTypes[SettingName], + ) => Promise>, + resetSetting: () => void, + ]; + export default useSetting; +} +declare module 'renderer/hooks/papi-hooks/use-project-data-provider.hook' { + import { ProjectDataProviders } from 'papi-shared-types'; + /** + * 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 + * 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 + */ + const useProjectDataProvider: ( + projectType: ProjectType, + projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined, + ) => ProjectDataProviders[ProjectType] | undefined; + export default useProjectDataProvider; +} +declare module 'renderer/hooks/papi-hooks/use-project-data.hook' { + import { + DataProviderSubscriberOptions, + DataProviderUpdateInstructions, + } from 'shared/models/data-provider.model'; + import { ProjectDataProviders, ProjectDataTypes, ProjectTypes } from 'papi-shared-types'; + /** + * React hook to use data from a project data provider + * + * @example `useProjectData('ParatextStandard', 'project id').VerseUSFM(...);` + */ + type UseProjectDataHook = { + ( + projectType: ProjectType, + projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined, + ): { + [TDataType in keyof ProjectDataTypes[ProjectType]]: ( + // @ts-ignore TypeScript pretends it can't find `selector`, but it works just fine + selector: ProjectDataTypes[ProjectType][TDataType]['selector'], + // @ts-ignore TypeScript pretends it can't find `getData`, but it works just fine + defaultValue: ProjectDataTypes[ProjectType][TDataType]['getData'], + subscriberOptions?: DataProviderSubscriberOptions, + ) => [ + // @ts-ignore TypeScript pretends it can't find `getData`, but it works just fine + ProjectDataTypes[ProjectType][TDataType]['getData'], + ( + | (( + // @ts-ignore TypeScript pretends it can't find `setData`, but it works just fine + newData: ProjectDataTypes[ProjectType][TDataType]['setData'], + ) => Promise>) + | undefined + ), + boolean, + ]; + }; + }; + /** + * ```typescript + * useProjectData( + * projectType: ProjectType, + * projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined, + * ).DataType( + * selector: ProjectDataTypes[ProjectType][DataType]['selector'], + * defaultValue: ProjectDataTypes[ProjectType][DataType]['getData'], + * subscriberOptions?: DataProviderSubscriberOptions, + * ) => [ + * ProjectDataTypes[ProjectType][DataType]['getData'], + * ( + * | (( + * newData: ProjectDataTypes[ProjectType][DataType]['setData'], + * ) => Promise>) + * | undefined + * ), + * boolean, + * ] + * ``` + * + * 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 + * with `useProjectData('', '').` and use like any other React + * hook. + * + * _@example_ Subscribing to Verse USFM info at JHN 11:35 on a `ParatextStandard` project with + * projectId `32664dc3288a28df2e2bb75ded887fc8f17a15fb`: + * + * ```typescript + * const [verse, setVerse, verseIsLoading] = useProjectData( + * 'ParatextStandard', + * '32664dc3288a28df2e2bb75ded887fc8f17a15fb', + * ).VerseUSFM( + * useMemo(() => new VerseRef('JHN', '11', '35', ScrVers.English), []), + * 'Loading verse ', + * ); + * ``` + * + * _@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 + * 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) + * + * _@param_ `selector` tells the provider what data this listener is listening for + * + * 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 data + * + * _@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 + * until `projectDataProviderSource` or `selector` changes. + * + * _@returns_ `[data, setData, isLoading]` + */ + const useProjectData: UseProjectDataHook; + export default useProjectData; +} +declare module 'renderer/hooks/papi-hooks/use-data-provider-multi.hook' { + import { DataProviderNames, DataProviders } from 'papi-shared-types'; + /** + * Gets an array of data providers based on an array of input sources + * + * @type `T` - The types of data providers to return. Use `IDataProvider`, + * specifying your own types, or provide a custom data provider type for each item in the array. + * Note that if you provide more than one data type, each item in the returned array will be + * considered to be any of those types. For example, if you call `useDataProviderMulti`, all items in the returned array will be considered to be of type `Type1 | Type2 | + * undefined`. Although you can determine the actual type based on the array index, TypeScript + * will not know, so you will need to type assert the array items for later type checking to + * work. + * @param dataProviderSources Array containing string names of the data providers to get OR data + * providers themselves (i.e., the results of useDataProvider/useDataProviderMulti) if you want + * this hook to return the data providers again. It is fine to have a mix of strings and data + * providers in the array. + * + * WARNING: THE ARRAY MUST BE STABLE - const or wrapped in useState, useMemo, etc. It must not be + * updated every render. + * @returns An array of data providers that correspond by index to the values in + * `dataProviderSources`. Each item in the array will be (a) undefined if the data provider has + * not been retrieved or has been disposed, or (b) a data provider if it has been retrieved and is + * not disposed. + */ + function useDataProviderMulti( + dataProviderSources: ( + | EachDataProviderName[number] + | DataProviders[EachDataProviderName[number]] + | undefined + )[], + ): (DataProviders[EachDataProviderName[number]] | undefined)[]; + export default useDataProviderMulti; +} +declare module 'renderer/hooks/papi-hooks/index' { + export { default as useDataProvider } from 'renderer/hooks/papi-hooks/use-data-provider.hook'; + 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 useDialogCallback } from 'renderer/hooks/papi-hooks/use-dialog-callback.hook'; + export { default as useDataProviderMulti } from 'renderer/hooks/papi-hooks/use-data-provider-multi.hook'; +} +declare module '@papi/frontend/react' { + export * from 'renderer/hooks/papi-hooks/index'; +} +declare module 'renderer/services/renderer-xml-http-request.service' { + /** This wraps the browser's XMLHttpRequest implementation to + * provide better control over internet access. It is isomorphic with the standard XMLHttpRequest, + * so it should act as a drop-in replacement. + * + * Note that Node doesn't have a native implementation, so this is only for the renderer. + */ + export default class PapiRendererXMLHttpRequest implements XMLHttpRequest { + readonly DONE: 4; + readonly HEADERS_RECEIVED: 2; + readonly LOADING: 3; + readonly OPENED: 1; + readonly UNSENT: 0; + abort: () => void; + addEventListener: ( + type: K, + listener: (this: XMLHttpRequest, ev: XMLHttpRequestEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ) => void; + dispatchEvent: (event: Event) => boolean; + getAllResponseHeaders: () => string; + getResponseHeader: (name: string) => string | null; + open: ( + method: string, + url: string, + async?: boolean, + username?: string | null, + password?: string | null, + ) => void; + onabort: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + onerror: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + onload: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + onloadend: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + onloadstart: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + onprogress: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + onreadystatechange: ((this: XMLHttpRequest, ev: Event) => any) | null; + ontimeout: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; + overrideMimeType: (mime: string) => void; + readyState: number; + removeEventListener: ( + type: K, + listener: (this: XMLHttpRequest, ev: XMLHttpRequestEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ) => void; + response: any; + responseText: string; + responseType: XMLHttpRequestResponseType; + responseURL: string; + responseXML: Document | null; + send: (body?: Document | XMLHttpRequestBodyInit | null) => void; + setRequestHeader: (name: string, value: string) => void; + status: number; + statusText: string; + timeout: number; + upload: XMLHttpRequestUpload; + withCredentials: boolean; + constructor(); + } +} +declare module '@papi/frontend' { + /** + * Unified module for accessing API features in the renderer. + * + * WARNING: DO NOT IMPORT papi IN ANY FILE THAT papi IMPORTS AND EXPOSES. + */ + import * as commandService from 'shared/services/command.service'; + import { PapiNetworkService } from 'shared/services/network.service'; + import { WebViewServiceType } from 'shared/services/web-view.service-model'; + import { InternetService } from 'shared/services/internet.service'; + import { DataProviderService } from 'shared/services/data-provider.service'; + import { ProjectLookupServiceType } from 'shared/services/project-lookup.service-model'; + import { PapiFrontendProjectDataProviderService } from 'shared/services/project-data-provider.service'; + import { ISettingsService } from 'shared/services/settings.service-model'; + import { DialogService } from 'shared/services/dialog.service-model'; + import * as papiReact from '@papi/frontend/react'; + import PapiRendererWebSocket from 'renderer/services/renderer-web-socket.service'; + import { IMenuDataService } from 'shared/services/menu-data.service-model'; + import PapiRendererXMLHttpRequest from 'renderer/services/renderer-xml-http-request.service'; + const papi: { + /** This is just an alias for internet.fetch */ + fetch: typeof globalThis.fetch; + /** This wraps the browser's WebSocket implementation to provide + * better control over internet access. It is isomorphic with the standard WebSocket, so it should + * act as a drop-in replacement. * - * @param token ExecutionToken provided to the extension when `activate()` was called - * @param fileName Name of the file to be read - * @returns Promise for a string with the contents of the file + * Note that the Node WebSocket implementation is different and not wrapped here. */ - function readTextFileFromInstallDirectory(token: ExecutionToken, fileName: string): Promise; - /** - * Read a binary file from the the extension's installation directory + WebSocket: typeof PapiRendererWebSocket; + /** This wraps the browser's XMLHttpRequest implementation to + * provide better control over internet access. It is isomorphic with the standard XMLHttpRequest, + * so it should act as a drop-in replacement. * - * @param token ExecutionToken provided to the extension when `activate()` was called - * @param fileName Name of the file to be read - * @returns Promise for a Buffer with the contents of the file + * Note that Node doesn't have a native implementation, so this is only for the renderer. */ - function readBinaryFileFromInstallDirectory(token: ExecutionToken, fileName: string): Promise; + XMLHttpRequest: typeof PapiRendererXMLHttpRequest; /** - * Read data specific to the user (as identified by the OS) and extension (as identified by the - * ExecutionToken) * - * @param token ExecutionToken provided to the extension when `activate()` was called - * @param key Unique identifier of the data - * @returns Promise for a string containing the data + * The command service allows you to exchange messages with other components in the platform. You + * can register a command that other services and extensions can send you. You can send commands to + * other services and extensions that have registered commands. */ - function readUserData(token: ExecutionToken, key: string): Promise; + commands: typeof commandService; /** - * Write data specific to the user (as identified by the OS) and extension (as identified by the - * ExecutionToken) * - * @param token ExecutionToken provided to the extension when `activate()` was called - * @param key Unique identifier of the data - * @param data Data to be written - * @returns Promise that will resolve if the data is written successfully - */ - function writeUserData(token: ExecutionToken, key: string, data: string): Promise; - /** - * Delete data previously written that is specific to the user (as identified by the OS) and - * extension (as identified by the ExecutionToken) + * Service exposing various functions related to using webViews * - * @param token ExecutionToken provided to the extension when `activate()` was called - * @param key Unique identifier of the data - * @returns Promise that will resolve if the data is deleted successfully + * WebViews are iframes in the Platform.Bible UI into which extensions load frontend code, either + * HTML or React components. */ - function deleteUserData(token: ExecutionToken, key: string): Promise; - export interface ExtensionStorageService { - readTextFileFromInstallDirectory: typeof readTextFileFromInstallDirectory; - readBinaryFileFromInstallDirectory: typeof readBinaryFileFromInstallDirectory; - readUserData: typeof readUserData; - writeUserData: typeof writeUserData; - deleteUserData: typeof deleteUserData; - } + webViews: WebViewServiceType; /** - * JSDOC SOURCE extensionStorageService * - * This service provides extensions in the extension host the ability to read/write data based on - * the extension identity and current user (as identified by the OS). This service will not work - * within the renderer. + * Prompt the user for responses with dialogs */ - const extensionStorageService: ExtensionStorageService; - export default extensionStorageService; -} -declare module "shared/models/dialog-options.model" { - /** General options to adjust dialogs (created from `papi.dialogs`) */ - export type DialogOptions = { - /** Dialog title to display in the header. Default depends on the dialog */ - title?: string; - /** Url of dialog icon to display in the header. Default is Platform.Bible logo */ - iconUrl?: string; - /** The message to show the user in the dialog. Default depends on the dialog */ - prompt?: string; - }; - /** Data in each tab that is a dialog. Added to DialogOptions in `dialog.service-host.ts` */ - export type DialogData = DialogOptions & { - isDialog: true; - }; -} -declare module "renderer/components/dialogs/dialog-base.data" { - import { FloatSize, TabLoader, TabSaver } from "shared/models/docking-framework.model"; - import { DialogData } from "shared/models/dialog-options.model"; - import { ReactElement } from 'react'; - /** Base type for DialogDefinition. Contains reasonable defaults for dialogs */ - export type DialogDefinitionBase = Readonly<{ - /** Overwritten in {@link DialogDefinition}. Must be specified by all DialogDefinitions */ - tabType?: string; - /** Overwritten in {@link DialogDefinition}. Must be specified by all DialogDefinitions */ - Component?: (props: DialogProps) => ReactElement; - /** - * The default icon for this dialog. This may be overridden by the `DialogOptions.iconUrl` - * - * Defaults to the Platform.Bible logo - */ - defaultIconUrl?: string; - /** - * The default title for this dialog. This may be overridden by the `DialogOptions.title` - * - * Defaults to the DialogDefinition's `tabType` - */ - defaultTitle?: string; - /** The width and height at which the dialog will be loaded in CSS `px` units */ - initialSize: FloatSize; - /** The minimum width to which the dialog can be set in CSS `px` units */ - minWidth?: number; - /** The minimum height to which the dialog can be set in CSS `px` units */ - minHeight?: number; - /** - * The function used to load the dialog into the dock layout. Default uses the `Component` field - * and passes in the `DialogProps` - */ - loadDialog: TabLoader; - /** - * The function used to save the dialog into the dock layout - * - * Default does not save the dialog as they cannot properly be restored yet. - * - * TODO: preserve requests between refreshes - save the dialog info in such a way that it works - * when loading again after refresh - */ - saveDialog: TabSaver; - }>; - /** Props provided to the dialog component */ - export type DialogProps = DialogData & { - /** - * Sends the data as a resolved response to the dialog request and closes the dialog - * - * @param data Data with which to resolve the request - */ - submitDialog(data: TData): void; - /** Cancels the dialog request (resolves the response with `undefined`) and closes the dialog */ - cancelDialog(): void; - /** - * Rejects the dialog request with the specified message and closes the dialog - * - * @param errorMessage Message to explain why the dialog request was rejected - */ - rejectDialog(errorMessage: string): void; - }; - /** - * Set the functionality of submitting and canceling dialogs. This should be called specifically by - * `dialog.service-host.ts` immediately on startup and by nothing else. This is only here to - * mitigate a dependency cycle - * - * @param dialogServiceFunctions Functions from the dialog service host for resolving and rejecting - * dialogs - */ - export function hookUpDialogService({ resolveDialogRequest: resolve, rejectDialogRequest: reject, }: { - resolveDialogRequest: (id: string, data: unknown | undefined) => void; - rejectDialogRequest: (id: string, message: string) => void; - }): void; - /** - * Static definition of a dialog that can be shown in Platform.Bible - * - * For good defaults, dialogs can include all the properties of this dialog. Dialogs must then - * specify `tabType` and `Component` in order to comply with `DialogDefinition` - * - * Note: this is not a class that can be inherited because all properties would be static but then - * we would not be able to use the default `loadDialog` because it would be using a static reference - * to a nonexistent `Component`. Instead of inheriting this as a class, any dialog definition can - * spread this `{ ...DIALOG_BASE }` - */ - const DIALOG_BASE: DialogDefinitionBase; - export default DIALOG_BASE; -} -declare module "renderer/components/dialogs/dialog-definition.model" { - import { DialogOptions } from "shared/models/dialog-options.model"; - import { DialogDefinitionBase, DialogProps } from "renderer/components/dialogs/dialog-base.data"; - import { ReactElement } from 'react'; - /** The tabType for the select project dialog in `select-project.dialog.tsx` */ - export const SELECT_PROJECT_DIALOG_TYPE = "platform.selectProject"; - /** The tabType for the select multiple projects dialog in `select-multiple-projects.dialog.tsx` */ - export const SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE = "platform.selectMultipleProjects"; - /** Options to provide when showing the Select Project dialog */ - export type SelectProjectDialogOptions = DialogOptions & { - /** Project IDs to exclude from showing in the dialog */ - excludeProjectIds?: string[]; - }; - /** Options to provide when showing the Select Multiple Project dialog */ - export type SelectMultipleProjectsDialogOptions = DialogOptions & { - /** Project IDs to exclude from showing in the dialog */ - excludeProjectIds?: string[]; - /** Project IDs that should start selected in the dialog */ - selectedProjectIds?: string[]; - }; + dialogs: DialogService; /** - * Mapped type for dialog functions to use in getting various types for dialogs * - * Keys should be dialog names, and values should be {@link DialogDataTypes} - * - * If you add a dialog here, you must also add it on {@link DIALOGS} + * Service that provides a way to send and receive network events */ - export interface DialogTypes { - [SELECT_PROJECT_DIALOG_TYPE]: DialogDataTypes; - [SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE]: DialogDataTypes; - } - /** Each type of dialog. These are the tab types used in the dock layout */ - export type DialogTabTypes = keyof DialogTypes; - /** Types related to a specific dialog */ - export type DialogDataTypes = { - /** - * The dialog options to specify when calling the dialog. Passed into `loadDialog` as - * SavedTabInfo.data - * - * The default implementation of `loadDialog` passes all the options down to the dialog component - * as props - */ - options: TOptions; - /** The type of the response to the dialog request */ - responseType: TReturnType; - /** Props provided to the dialog component */ - props: DialogProps & TOptions; - }; - export type DialogDefinition = Readonly & DialogTypes[DialogTabType]['options']) => ReactElement; - }>; -} -declare module "shared/services/dialog.service-model" { - import { DialogTabTypes, DialogTypes } from "renderer/components/dialogs/dialog-definition.model"; - import { DialogOptions } from "shared/models/dialog-options.model"; - /** - * JSDOC SOURCE dialogService + network: PapiNetworkService; + /** * - * Prompt the user for responses with dialogs + * All extensions and services should use this logger to provide a unified output of logs */ - export interface DialogService { - /** - * Shows a dialog to the user and prompts the user to respond - * - * @type `TReturn` - The type of data the dialog responds with - * @param dialogType The type of dialog to show the user - * @param options Various options for configuring the dialog that shows - * @returns Returns the user's response or `undefined` if the user cancels - */ - showDialog(dialogType: DialogTabType, options?: DialogTypes[DialogTabType]['options']): Promise; - /** - * Shows a select project dialog to the user and prompts the user to select a dialog - * - * @param options Various options for configuring the dialog that shows - * @returns Returns the user's selected project id or `undefined` if the user cancels - */ - selectProject(options?: DialogOptions): Promise; - } - /** Prefix on requests that indicates that the request is related to dialog operations */ - export const CATEGORY_DIALOG = "dialog"; -} -declare module "shared/services/dialog.service" { - import { DialogService } from "shared/services/dialog.service-model"; - const dialogService: DialogService; - export default dialogService; -} -declare module "extension-host/extension-types/extension-activation-context.model" { - import { ExecutionToken } from "node/models/execution-token.model"; - import { UnsubscriberAsyncList } from 'platform-bible-utils'; - /** An object of this type is passed into `activate()` for each extension during initialization */ - export type ExecutionActivationContext = { - /** Canonical name of the extension */ - name: string; - /** Used to save and load data from the storage service. */ - executionToken: ExecutionToken; - /** Tracks all registrations made by an extension so they can be cleaned up when it is unloaded */ - registrations: UnsubscriberAsyncList; - }; -} -declare module "renderer/hooks/papi-hooks/use-dialog-callback.hook" { - import { DialogTabTypes, DialogTypes } from "renderer/components/dialogs/dialog-definition.model"; - export type UseDialogCallbackOptions = { - /** - * How many dialogs are allowed to be open at once from this dialog callback. Calling the callback - * when this number of maximum open dialogs has been reached does nothing. Set to -1 for - * unlimited. Defaults to 1. - */ - maximumOpenDialogs?: number; + logger: import('electron-log').MainLogger & { + default: import('electron-log').MainLogger; }; /** - * JSDOC SOURCE useDialogCallback - * - * Enables using `papi.dialogs.showDialog` in React more easily. Returns a callback to run that will - * open a dialog with the provided `dialogType` and `options` then run the `resolveCallback` with - * the dialog response or `rejectCallback` if there is an error. By default, only one dialog can be - * open at a time. - * - * If you need to open multiple dialogs and track which dialog is which, you can set - * `options.shouldOpenMultipleDialogs` to `true` and add a counter to the `options` when calling the - * callback. Then `resolveCallback` will be resolved with that options object including your - * counter. - * - * @type `DialogTabType` The dialog type you are using. Should be inferred by parameters - * @param dialogType Dialog type you want to show on the screen - * - * 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 updating this parameter will not cause a new - * callback to be returned. However, because of the nature of calling dialogs, this has no adverse - * effect on the functionality of this hook. Calling the callback will always use the latest - * `dialogType`. - * @param options Various options for configuring the dialog that shows and this hook. If an - * `options` parameter is also provided to the returned `showDialog` callback, those - * callback-provided `options` merge over these hook-provided `options` - * - * 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 updating this parameter will not cause a new - * callback to be returned. However, because of the nature of calling dialogs, this has no adverse - * effect on the functionality of this hook. Calling the callback will always use the latest - * `options`. - * @param resolveCallback `(response, dialogType, options)` The function that will be called if the - * dialog request resolves properly - * - * - `response` - the resolved value of the dialog call. Either the user's response or `undefined` if - * the user cancels - * - `dialogType` - the value of `dialogType` at the time that this dialog was called - * - `options` the `options` provided to the dialog at the time that this dialog was called. This - * consists of the `options` provided to the returned `showDialog` callback merged over the - * `options` provided to the hook and additionally contains {@link UseDialogCallbackOptions} - * properties - * - * 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 updating this parameter will not cause a new - * callback to be returned. However, because of the nature of calling dialogs, this has no adverse - * effect on the functionality of this hook. When the dialog resolves, it will always call the - * latest `resolveCallback`. - * @param rejectCallback `(error, dialogType, options)` The function that will be called if the - * dialog request throws an error - * - * - `error` - the error thrown while calling the dialog - * - `dialogType` - the value of `dialogType` at the time that this dialog was called - * - `options` the `options` provided to the dialog at the time that this dialog was called. This - * consists of the `options` provided to the returned `showDialog` callback merged over the - * `options` provided to the hook and additionally contains {@link UseDialogCallbackOptions} - * properties - * - * 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 updating this parameter will not cause a new - * callback to be returned. However, because of the nature of calling dialogs, this has no adverse - * effect on the functionality of this hook. If the dialog throws an error, it will always call - * the latest `rejectCallback`. - * @returns `showDialog(options?)` - callback to run to show the dialog to prompt the user for a - * response - * - * - `optionsOverrides?` - `options` object you may specify that will merge over the `options` you - * provide to the hook before passing to the dialog. All properties are optional, so you may - * specify as many or as few properties here as you want to overwrite the properties in the - * `options` you provide to the hook - */ - function useDialogCallback(dialogType: DialogTabType, options: DialogOptions & UseDialogCallbackOptions, resolveCallback: (response: DialogTypes[DialogTabType]['responseType'] | undefined, dialogType: DialogTabType, options: DialogOptions) => void, rejectCallback: (error: unknown, dialogType: DialogTabType, options: DialogOptions) => void): (optionOverrides?: Partial) => Promise; - /** JSDOC DESTINATION useDialogCallback */ - function useDialogCallback(dialogType: DialogTabType, options: DialogOptions & UseDialogCallbackOptions, resolveCallback: (response: DialogTypes[DialogTabType]['responseType'] | undefined, dialogType: DialogTabType, options: DialogOptions) => void): (optionOverrides?: Partial) => Promise; - export default useDialogCallback; -} -declare module "shared/services/papi-core.service" { - /** Exporting empty object so people don't have to put 'type' in their import statements */ - const core: {}; - export default core; - export type { ExecutionActivationContext } from "extension-host/extension-types/extension-activation-context.model"; - export type { ExecutionToken } from "node/models/execution-token.model"; - export type { DialogTypes } from "renderer/components/dialogs/dialog-definition.model"; - export type { UseDialogCallbackOptions } from "renderer/hooks/papi-hooks/use-dialog-callback.hook"; - export type { default as IDataProvider } from "shared/models/data-provider.interface"; - export type { DataProviderUpdateInstructions, DataProviderDataType, DataProviderSubscriberOptions, } from "shared/models/data-provider.model"; - export type { WithNotifyUpdate } from "shared/models/data-provider-engine.model"; - export type { default as IDataProviderEngine } from "shared/models/data-provider-engine.model"; - export type { DialogOptions } from "shared/models/dialog-options.model"; - export type { ExtensionDataScope, MandatoryProjectDataType, } from "shared/models/project-data-provider.model"; - export type { ProjectMetadata } from "shared/models/project-metadata.model"; - export type { GetWebViewOptions, SavedWebViewDefinition, UseWebViewStateHook, WebViewContentType, WebViewDefinition, WebViewProps, } from "shared/models/web-view.model"; - export type { IWebViewProvider } from "shared/models/web-view-provider.model"; -} -declare module "shared/services/menu-data.service-model" { - import { OnDidDispose, UnsubscriberAsync, MultiColumnMenu, ReferencedItem, WebViewMenu } from 'platform-bible-utils'; - import { DataProviderDataType, DataProviderSubscriberOptions, DataProviderUpdateInstructions } from "shared/models/data-provider.model"; - import { IDataProvider } from "shared/services/papi-core.service"; - /** JSDOC DESTINATION menuDataServiceProviderName */ - export const menuDataServiceProviderName = "platform.menuDataServiceDataProvider"; - export const menuDataServiceObjectToProxy: Readonly<{ - /** - * JSDOC SOURCE menuDataServiceProviderName - * - * This name is used to register the menu data data provider on the papi. You can use this name to - * find the data provider when accessing it using the useData hook - */ - dataProviderName: "platform.menuDataServiceDataProvider"; - }>; - export type MenuDataDataTypes = { - MainMenu: DataProviderDataType; - WebViewMenu: DataProviderDataType; - }; - module 'papi-shared-types' { - interface DataProviders { - [menuDataServiceProviderName]: IMenuDataService; - } - } - /** - * JSDOC SOURCE menuDataService * - * Service that allows to get and store menu data + * Service that provides a way to call `fetch` since the original function is not available */ - export type IMenuDataService = { - /** - * JSDOC SOURCE getMainMenu - * - * Get menu content for the main menu - * - * @param mainMenuType Does not have to be defined - * @returns MultiColumnMenu object of main menu content - */ - getMainMenu(mainMenuType: undefined): Promise; - /** JSDOC DESTINATION getMainMenu */ - getMainMenu(): Promise; - /** - * This data cannot be changed. Trying to use this setter this will always throw - * - * @param mainMenuType Does not have to be defined - * @param value MultiColumnMenu object to set as the main menu - * @returns Unsubscriber function - */ - setMainMenu(mainMenuType: undefined, value: never): Promise>; - /** - * Subscribe to run a callback function when the main menu data is changed - * - * @param mainMenuType Does not have to be defined - * @param callback Function to run with the updated menuContent for this selector - * @param options Various options to adjust how the subscriber emits updates - * @returns Unsubscriber function (run to unsubscribe from listening for updates) - */ - subscribeMainMenu(mainMenuType: undefined, callback: (menuContent: MultiColumnMenu) => void, options?: DataProviderSubscriberOptions): Promise; - /** - * Get menu content for a web view - * - * @param webViewType The type of webview for which a menu should be retrieved - * @returns WebViewMenu object of web view menu content - */ - getWebViewMenu(webViewType: ReferencedItem): Promise; - /** - * This data cannot be changed. Trying to use this setter this will always throw - * - * @param webViewType The type of webview for which a menu should be set - * @param value Menu of specified webViewType - * @returns Unsubscriber function - */ - setWebViewMenu(webViewType: ReferencedItem, value: never): Promise>; - /** - * Subscribe to run a callback function when the web view menu data is changed - * - * @param webViewType The type of webview for which a menu should be subscribed - * @param callback Function to run with the updated menuContent for this selector - * @param options Various options to adjust how the subscriber emits updates - * @returns Unsubscriber function (run to unsubscribe from listening for updates) - */ - subscribeWebViewMenu(webViewType: ReferencedItem, callback: (menuContent: WebViewMenu) => void, options?: DataProviderSubscriberOptions): Promise; - } & OnDidDispose & typeof menuDataServiceObjectToProxy & IDataProvider; -} -declare module "shared/services/menu-data.service" { - import { IMenuDataService } from "shared/services/menu-data.service-model"; - const menuDataService: IMenuDataService; - export default menuDataService; -} -declare module "extension-host/services/papi-backend.service" { - /** - * Unified module for accessing API features in the extension host. - * - * WARNING: DO NOT IMPORT papi IN ANY FILE THAT papi IMPORTS AND EXPOSES. - */ - import * as commandService from "shared/services/command.service"; - import { PapiNetworkService } from "shared/services/network.service"; - 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 { 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"; - const papi: { - /** JSDOC DESTINATION DataProviderEngine */ - DataProviderEngine: typeof PapiDataProviderEngine; - /** This is just an alias for internet.fetch */ - fetch: typeof globalThis.fetch; - /** JSDOC DESTINATION commandService */ - commands: typeof commandService; - /** JSDOC DESTINATION papiWebViewService */ - webViews: WebViewServiceType; - /** JSDOC DESTINATION papiWebViewProviderService */ - webViewProviders: PapiWebViewProviderService; - /** JSDOC DESTINATION dialogService */ - dialogs: DialogService; - /** JSDOC DESTINATION papiNetworkService */ - network: PapiNetworkService; - /** JSDOC DESTINATION logger */ - logger: import("electron-log").MainLogger & { - default: import("electron-log").MainLogger; - }; - /** JSDOC DESTINATION internetService */ - internet: InternetService; - /** JSDOC DESTINATION dataProviderService */ - dataProviders: DataProviderService; - /** JSDOC DESTINATION papiBackendProjectDataProviderService */ - projectDataProviders: PapiBackendProjectDataProviderService; - /** JSDOC DESTINATION projectLookupService */ - projectLookup: ProjectLookupServiceType; - /** JSDOC DESTINATION extensionStorageService */ - storage: ExtensionStorageService; - /** JSDOC DESTINATION menuDataService */ - menuData: IMenuDataService; - }; - export default papi; - /** JSDOC DESTINATION DataProviderEngine */ - export const DataProviderEngine: typeof PapiDataProviderEngine; - /** This is just an alias for internet.fetch */ - export const fetch: typeof globalThis.fetch; - /** JSDOC DESTINATION commandService */ - export const commands: typeof commandService; - /** JSDOC DESTINATION papiWebViewService */ - export const webViews: WebViewServiceType; - /** JSDOC DESTINATION papiWebViewProviderService */ - export const webViewProviders: PapiWebViewProviderService; - /** JSDOC DESTINATION dialogService */ - export const dialogs: DialogService; - /** JSDOC DESTINATION papiNetworkService */ - export const network: PapiNetworkService; - /** JSDOC DESTINATION logger */ - export const logger: import("electron-log").MainLogger & { - default: import("electron-log").MainLogger; - }; - /** JSDOC DESTINATION internetService */ - export const internet: InternetService; - /** JSDOC DESTINATION dataProviderService */ - export const dataProviders: DataProviderService; - /** JSDOC DESTINATION papiBackendProjectDataProviderService */ - export const projectDataProviders: PapiBackendProjectDataProviderService; - /** JSDOC DESTINATION projectLookupService */ - export const projectLookup: ProjectLookupServiceType; - /** JSDOC DESTINATION extensionStorageService */ - export const storage: ExtensionStorageService; - /** JSDOC DESTINATION menuDataService */ - export const menuData: IMenuDataService; -} -declare module "extension-host/extension-types/extension.interface" { - import { UnsubscriberAsync } from 'platform-bible-utils'; - import { ExecutionActivationContext } from "extension-host/extension-types/extension-activation-context.model"; - /** Interface for all extensions to implement */ - export interface IExtension { - /** - * Sets up this extension! Runs when paranext wants this extension to activate. For example, - * activate() should register commands for this extension - * - * @param context Data and utilities that are specific to this particular extension - */ - activate: (context: ExecutionActivationContext) => Promise; - /** - * Deactivate anything in this extension that is not covered by the registrations in the context - * object given to activate(). - * - * @returns Promise that resolves to true if successfully deactivated - */ - deactivate?: UnsubscriberAsync; - } -} -declare module "extension-host/extension-types/extension-manifest.model" { - /** Information about an extension provided by the extension developer. */ - export type ExtensionManifest = { - /** Name of the extension */ - name: string; - /** - * Extension version - expected to be [semver](https://semver.org/) like `"0.1.3"`. - * - * Note: semver may become a hard requirement in the future, so we recommend using it now. - */ - version: string; - /** - * Path to the JavaScript file to run in the extension host. Relative to the extension's root - * folder. - * - * Must be specified. Can be an empty string if the extension does not have any JavaScript to run. - */ - main: string; - /** - * Path to the TypeScript type declaration file that describes this extension and its interactions - * on the PAPI. Relative to the extension's root folder. - * - * If not provided, Platform.Bible will look in the following locations: - * - * 1. `.d.ts` - * 2. `.d.ts` - * 3. `index.d.ts` - * - * See [Extension Anatomy - Type Declaration - * Files](https://github.com/paranext/paranext-extension-template/wiki/Extension-Anatomy#type-declaration-files-dts) - * for more information about extension type declaration files. - */ - types?: string; - /** - * List of events that occur that should cause this extension to be activated. Not yet - * implemented. - */ - activationEvents: string[]; - }; -} -declare module "shared/services/settings.service-model" { - import { SettingNames, SettingTypes } from 'papi-shared-types'; - import { OnDidDispose, PlatformEventEmitter, Unsubscriber } from 'platform-bible-utils'; - import { DataProviderUpdateInstructions, IDataProvider } from "shared/services/papi-core.service"; - /** JSDOC DESTINATION dataProviderName */ - export const settingsServiceDataProviderName = "platform.settingsServiceDataProvider"; - export const settingsServiceObjectToProxy: Readonly<{ - /** - * JSDOC SOURCE dataProviderName - * - * Name used to register the data provider - * - * You can use this name - */ - dataProviderName: "platform.settingsServiceDataProvider"; - }>; - /** - * SettingDataTypes handles getting and setting Platform.Bible core application and extension - * settings. - * - * Note: the unnamed (`''`) data type is not actually part of `SettingDataTypes` because the methods - * would not be able to create a generic type extending from `SettingNames` in order to return the - * specific setting type being requested. As such, `get`, `set`, `reset` and `subscribe` are all - * specified on {@link ISettingsService} instead. Unfortunately, as a result, using Intellisense with - * `useData` will not show the unnamed data type (`''`) as an option, but you can use `useSetting` - * instead. However, do note that the unnamed data type (`''`) is fully functional. - */ - export type SettingDataTypes = {}; - module 'papi-shared-types' { - interface DataProviders { - [settingsServiceDataProviderName]: ISettingsService; - } - } - /** Event to set or update a setting */ - export type UpdateSettingEvent = { - type: 'update-setting'; - setting: SettingTypes[SettingName]; - }; - /** Event to remove a setting */ - export type ResetSettingEvent = { - type: 'reset-setting'; - }; - /** All supported setting events */ - export type SettingEvent = UpdateSettingEvent | ResetSettingEvent; - /** All message subscriptions - emitters that emit an event each time a setting is updated */ - export const onDidUpdateSettingEmitters: Map>>; - /** JSDOC SOURCE settingsService */ - export type ISettingsService = { - /** - * Retrieves the value of the specified setting - * - * @param key The string id of the setting for which the value is being retrieved - * @param defaultSetting The default value used for the setting if no value is available for the - * key - * @returns The value of the specified setting, parsed to an object. Returns default setting if - * setting does not exist - */ - get(key: SettingName): Promise; - /** - * Sets the value of the specified setting - * - * @param key The string id of the setting for which the value is being retrieved - * @param newSetting The value that is to be stored. Setting the new value to `undefined` is the - * equivalent of deleting the setting - */ - set(key: SettingName, newSetting: SettingTypes[SettingName]): Promise>; - /** - * Removes the setting from memory - * - * @param key The string id of the setting for which the value is being removed - * @returns `true` if successfully reset the project setting. `false` otherwise - */ - reset(key: SettingName): Promise; - /** - * Subscribes to updates of the specified setting. Whenever the value of the setting changes, the - * callback function is executed. - * - * @param key The string id of the setting for which the value is being subscribed to - * @param callback The function that will be called whenever the specified setting is updated - * @returns Unsubscriber that should be called whenever the subscription should be deleted - */ - subscribe(key: SettingName, callback: (newSetting: SettingEvent) => void): Promise; - } & OnDidDispose & IDataProvider & typeof settingsServiceObjectToProxy; -} -declare module "shared/services/settings.service" { - import { ISettingsService } from "shared/services/settings.service-model"; - const settingsService: ISettingsService; - export default settingsService; -} -declare module "renderer/hooks/hook-generators/create-use-network-object-hook.util" { - import { NetworkObject } from "shared/models/network-object.model"; - /** - * This function takes in a getNetworkObject function and creates a hook with that function in it - * which will return a network object - * - * @param getNetworkObject A function that takes in an id string and returns a network object - * @param mapParametersToNetworkObjectSource Function that takes the parameters passed into the hook - * and returns the `networkObjectSource` associated with those parameters. Defaults to taking the - * first parameter passed into the hook and using that as the `networkObjectSource`. - * - * - Note: `networkObjectSource` is string name of the network object to get OR `networkObject` - * (result of this hook, if you want this hook to just return the network object again) - * - * @returns A function that takes in a networkObjectSource and returns a NetworkObject - */ - function createUseNetworkObjectHook(getNetworkObject: (...args: THookParams) => Promise | undefined>, mapParametersToNetworkObjectSource?: (...args: THookParams) => string | NetworkObject | undefined): (...args: THookParams) => NetworkObject | undefined; - export default createUseNetworkObjectHook; -} -declare module "renderer/hooks/papi-hooks/use-data-provider.hook" { - import { DataProviders } from 'papi-shared-types'; - /** - * Gets a data provider with specified provider name - * - * @type `T` - The type of data provider to return. Use `IDataProvider`, - * specifying your own types, or provide a custom data provider type - * @param dataProviderSource String name of the data provider to get OR dataProvider (result of - * useDataProvider, if you want this hook to just return the data provider again) - * @returns Undefined if the data provider has not been retrieved, data provider if it has been - * retrieved and is not disposed, and undefined again if the data provider is disposed - */ - const useDataProvider: (dataProviderSource: DataProviderName | DataProviders[DataProviderName] | undefined) => DataProviders[DataProviderName] | undefined; - export default useDataProvider; -} -declare module "renderer/hooks/hook-generators/create-use-data-hook.util" { - import { DataProviderSubscriberOptions, DataProviderUpdateInstructions } from "shared/models/data-provider.model"; - import IDataProvider from "shared/models/data-provider.interface"; - import ExtractDataProviderDataTypes from "shared/models/extract-data-provider-data-types.model"; - /** - * The final function called as part of the `useData` hook that is the actual React hook - * - * This is the `.Greeting(...)` part of `useData('helloSomeone.people').Greeting(...)` - */ - type UseDataFunctionWithProviderType, TDataType extends keyof ExtractDataProviderDataTypes> = (selector: ExtractDataProviderDataTypes[TDataType]['selector'], defaultValue: ExtractDataProviderDataTypes[TDataType]['getData'], subscriberOptions?: DataProviderSubscriberOptions) => [ - ExtractDataProviderDataTypes[TDataType]['getData'], - (((newData: ExtractDataProviderDataTypes[TDataType]['setData']) => Promise>>) | undefined), - boolean - ]; - /** - * A proxy that serves the actual hooks for a single data provider - * - * This is the `useData('helloSomeone.people')` part of - * `useData('helloSomeone.people').Greeting(...)` - */ - type UseDataProxy> = { - [TDataType in keyof ExtractDataProviderDataTypes]: UseDataFunctionWithProviderType; - }; + internet: InternetService; /** - * React hook to use data provider data with various data types - * - * @example `useData('helloSomeone.people').Greeting('Bill', 'Greeting loading')` * - * @type `TDataProvider` - The type of data provider to get. Use - * `IDataProvider`, specifying your own types, or provide a custom data - * provider type + * Service that allows extensions to send and receive data to/from other extensions */ - type UseDataHookGeneric = { - >(...args: TUseDataProviderParams): UseDataProxy; - }; + dataProviders: DataProviderService; /** - * Create a `useData(...).DataType(selector, defaultValue, options)` hook for a specific subset of - * data providers as supported by `useDataProviderHook` * - * @param useDataProviderHook Hook that gets a data provider from a specific subset of data - * providers - * @returns `useData` hook for getting data from a data provider + * Service that gets project data providers */ - function createUseDataHook(useDataProviderHook: (...args: TUseDataProviderParams) => IDataProvider | undefined): UseDataHookGeneric; - export default createUseDataHook; -} -declare module "renderer/hooks/papi-hooks/use-data.hook" { - import { DataProviderSubscriberOptions, DataProviderUpdateInstructions } from "shared/models/data-provider.model"; - import { DataProviderNames, DataProviderTypes, DataProviders } from 'papi-shared-types'; + projectDataProviders: PapiFrontendProjectDataProviderService; /** - * React hook to use data from a data provider * - * @example `useData('helloSomeone.people').Greeting('Bill', 'Greeting loading')` + * Provides metadata for projects known by the platform */ - type UseDataHook = { - (dataProviderSource: DataProviderName | DataProviders[DataProviderName] | undefined): { - [TDataType in keyof DataProviderTypes[DataProviderName]]: (selector: DataProviderTypes[DataProviderName][TDataType]['selector'], defaultValue: DataProviderTypes[DataProviderName][TDataType]['getData'], subscriberOptions?: DataProviderSubscriberOptions) => [ - DataProviderTypes[DataProviderName][TDataType]['getData'], - (((newData: DataProviderTypes[DataProviderName][TDataType]['setData']) => Promise>) | undefined), - boolean - ]; - }; - }; + projectLookup: ProjectLookupServiceType; /** - * ```typescript - * useData( - * dataProviderSource: DataProviderName | DataProviders[DataProviderName] | undefined, - * ).DataType( - * selector: DataProviderTypes[DataProviderName][DataType]['selector'], - * defaultValue: DataProviderTypes[DataProviderName][DataType]['getData'], - * subscriberOptions?: DataProviderSubscriberOptions, - * ) => [ - * DataProviderTypes[DataProviderName][DataType]['getData'], - * ( - * | (( - * newData: DataProviderTypes[DataProviderName][DataType]['setData'], - * ) => Promise>) - * | undefined - * ), - * boolean, - * ] - * ``` * - * React hook to use data from a data provider. Subscribes to run a callback on a data provider's - * data with specified selector on the specified data type that data provider serves. - * - * Usage: Specify the data provider and the data type on the data provider with - * `useData('').` and use like any other React hook. - * - * _@example_ Subscribing to Verse data at JHN 11:35 on the `'quickVerse.quickVerse'` data provider: - * - * ```typescript - * const [verseText, setVerseText, verseTextIsLoading] = useData('quickVerse.quickVerse').Verse( - * 'JHN 11:35', - * 'Verse text goes here', - * ); - * ``` - * - * _@param_ `dataProviderSource` string name of data provider to get OR dataProvider (result of - * useDataProvider if you want to consolidate and only get the data provider once) - * - * _@param_ `selector` tells the provider what data this listener is listening for - * - * 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 data - * - * _@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_ `[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 - * - `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 - * 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 - * provider - */ - const useData: UseDataHook; - export default useData; -} -declare module "renderer/hooks/papi-hooks/use-setting.hook" { - import { SettingTypes } from 'papi-shared-types'; - import { DataProviderSubscriberOptions, DataProviderUpdateInstructions } from "shared/models/data-provider.model"; - import { SettingDataTypes } from "shared/services/settings.service-model"; - /** - * Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes - * and gets updated when the setting is changed by others. - * - * @param key The string id that is used to store the setting in local storage - * - * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be - * updated every render - * @param defaultState The default state of the setting. If the setting already has a value set to - * it in local storage, this parameter will be ignored. - * - * Note: this parameter is internally assigned to a `ref`, so changing it will not cause any hooks - * to re-run with its new value. Running `resetSetting()` will always update the setting value - * returned to the latest `defaultState`, and changing the `key` will use the latest - * `defaultState`. However, if `defaultState` is changed while a setting is `defaultState` - * (meaning it is reset and has no value), the returned setting value will not be updated to the - * new `defaultState`. - * @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 state of the setting, either `defaultState` or the stored state on the - * papi, if any - * - `setSetting`: Function that updates the setting to a new value - * - `resetSetting`: Function that removes the setting and resets the value to `defaultState` - * - * @throws When subscription callback function is called with an update that has an unexpected - * message type - */ - const useSetting: (key: SettingName, defaultState: SettingTypes[SettingName], subscriberOptions?: DataProviderSubscriberOptions) => [setting: SettingTypes[SettingName], setSetting: (newData: SettingTypes[SettingName]) => Promise>, resetSetting: () => void]; - export default useSetting; -} -declare module "renderer/hooks/papi-hooks/use-project-data-provider.hook" { - import { ProjectDataProviders } from 'papi-shared-types'; - /** - * 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 - * 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 - */ - const useProjectDataProvider: (projectType: ProjectType, projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined) => ProjectDataProviders[ProjectType] | undefined; - export default useProjectDataProvider; -} -declare module "renderer/hooks/papi-hooks/use-project-data.hook" { - import { DataProviderSubscriberOptions, DataProviderUpdateInstructions } from "shared/models/data-provider.model"; - import { ProjectDataProviders, ProjectDataTypes, ProjectTypes } from 'papi-shared-types'; - /** - * React hook to use data from a project data provider - * - * @example `useProjectData('ParatextStandard', 'project id').VerseUSFM(...);` - */ - type UseProjectDataHook = { - (projectType: ProjectType, projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined): { - [TDataType in keyof ProjectDataTypes[ProjectType]]: (selector: ProjectDataTypes[ProjectType][TDataType]['selector'], defaultValue: ProjectDataTypes[ProjectType][TDataType]['getData'], subscriberOptions?: DataProviderSubscriberOptions) => [ - ProjectDataTypes[ProjectType][TDataType]['getData'], - (((newData: ProjectDataTypes[ProjectType][TDataType]['setData']) => Promise>) | undefined), - boolean - ]; - }; - }; + * React hooks that enable interacting with the `papi` in React components more easily. + */ + react: typeof papiReact; + /** */ + settings: ISettingsService; /** - * ```typescript - * useProjectData( - * projectType: ProjectType, - * projectDataProviderSource: string | ProjectDataProviders[ProjectType] | undefined, - * ).DataType( - * selector: ProjectDataTypes[ProjectType][DataType]['selector'], - * defaultValue: ProjectDataTypes[ProjectType][DataType]['getData'], - * subscriberOptions?: DataProviderSubscriberOptions, - * ) => [ - * ProjectDataTypes[ProjectType][DataType]['getData'], - * ( - * | (( - * newData: ProjectDataTypes[ProjectType][DataType]['setData'], - * ) => Promise>) - * | undefined - * ), - * boolean, - * ] - * ``` - * - * 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 - * with `useProjectData('', '').` and use like any other React - * hook. * - * _@example_ Subscribing to Verse USFM info at JHN 11:35 on a `ParatextStandard` project with - * projectId `32664dc3288a28df2e2bb75ded887fc8f17a15fb`: - * - * ```typescript - * const [verse, setVerse, verseIsLoading] = useProjectData( - * 'ParatextStandard', - * '32664dc3288a28df2e2bb75ded887fc8f17a15fb', - * ).VerseUSFM( - * useMemo(() => new VerseRef('JHN', '11', '35', ScrVers.English), []), - * 'Loading verse ', - * ); - * ``` - * - * _@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 - * 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) - * - * _@param_ `selector` tells the provider what data this listener is listening for - * - * 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 data - * - * _@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 - * until `projectDataProviderSource` or `selector` changes. - * - * _@returns_ `[data, setData, isLoading]` - */ - const useProjectData: UseProjectDataHook; - export default useProjectData; -} -declare module "renderer/hooks/papi-hooks/use-data-provider-multi.hook" { - import { DataProviderNames, DataProviders } from 'papi-shared-types'; - /** - * Gets an array of data providers based on an array of input sources - * - * @type `T` - The types of data providers to return. Use `IDataProvider`, - * specifying your own types, or provide a custom data provider type for each item in the array. - * Note that if you provide more than one data type, each item in the returned array will be - * considered to be any of those types. For example, if you call `useDataProviderMulti`, all items in the returned array will be considered to be of type `Type1 | Type2 | - * undefined`. Although you can determine the actual type based on the array index, TypeScript - * will not know, so you will need to type assert the array items for later type checking to - * work. - * @param dataProviderSources Array containing string names of the data providers to get OR data - * providers themselves (i.e., the results of useDataProvider/useDataProviderMulti) if you want - * this hook to return the data providers again. It is fine to have a mix of strings and data - * providers in the array. - * - * WARNING: THE ARRAY MUST BE STABLE - const or wrapped in useState, useMemo, etc. It must not be - * updated every render. - * @returns An array of data providers that correspond by index to the values in - * `dataProviderSources`. Each item in the array will be (a) undefined if the data provider has - * not been retrieved or has been disposed, or (b) a data provider if it has been retrieved and is - * not disposed. - */ - function useDataProviderMulti(dataProviderSources: (EachDataProviderName[number] | DataProviders[EachDataProviderName[number]] | undefined)[]): (DataProviders[EachDataProviderName[number]] | undefined)[]; - export default useDataProviderMulti; -} -declare module "renderer/hooks/papi-hooks/index" { - export { default as useDataProvider } from "renderer/hooks/papi-hooks/use-data-provider.hook"; - 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 useDialogCallback } from "renderer/hooks/papi-hooks/use-dialog-callback.hook"; - export { default as useDataProviderMulti } from "renderer/hooks/papi-hooks/use-data-provider-multi.hook"; -} -declare module "renderer/services/papi-frontend-react.service" { - export * from "renderer/hooks/papi-hooks/index"; -} -declare module "renderer/services/renderer-xml-http-request.service" { - /** - * JSDOC SOURCE PapiRendererXMLHttpRequest This wraps the browser's XMLHttpRequest implementation to - * provide better control over internet access. It is isomorphic with the standard XMLHttpRequest, - * so it should act as a drop-in replacement. - * - * Note that Node doesn't have a native implementation, so this is only for the renderer. + * Service that allows to get and store menu data */ - export default class PapiRendererXMLHttpRequest implements XMLHttpRequest { - readonly DONE: 4; - readonly HEADERS_RECEIVED: 2; - readonly LOADING: 3; - readonly OPENED: 1; - readonly UNSENT: 0; - abort: () => void; - addEventListener: (type: K, listener: (this: XMLHttpRequest, ev: XMLHttpRequestEventMap[K]) => any, options?: boolean | AddEventListenerOptions) => void; - dispatchEvent: (event: Event) => boolean; - getAllResponseHeaders: () => string; - getResponseHeader: (name: string) => string | null; - open: (method: string, url: string, async?: boolean, username?: string | null, password?: string | null) => void; - onabort: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - onerror: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - onload: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - onloadend: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - onloadstart: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - onprogress: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - onreadystatechange: ((this: XMLHttpRequest, ev: Event) => any) | null; - ontimeout: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null; - overrideMimeType: (mime: string) => void; - readyState: number; - removeEventListener: (type: K, listener: (this: XMLHttpRequest, ev: XMLHttpRequestEventMap[K]) => any, options?: boolean | EventListenerOptions) => void; - response: any; - responseText: string; - responseType: XMLHttpRequestResponseType; - responseURL: string; - responseXML: Document | null; - send: (body?: Document | XMLHttpRequestBodyInit | null) => void; - setRequestHeader: (name: string, value: string) => void; - status: number; - statusText: string; - timeout: number; - upload: XMLHttpRequestUpload; - withCredentials: boolean; - constructor(); - } -} -declare module "renderer/services/papi-frontend.service" { - /** - * Unified module for accessing API features in the renderer. - * - * WARNING: DO NOT IMPORT papi IN ANY FILE THAT papi IMPORTS AND EXPOSES. - */ - import * as commandService from "shared/services/command.service"; - import { PapiNetworkService } from "shared/services/network.service"; - import { WebViewServiceType } from "shared/services/web-view.service-model"; - import { InternetService } from "shared/services/internet.service"; - import { DataProviderService } from "shared/services/data-provider.service"; - import { ProjectLookupServiceType } from "shared/services/project-lookup.service-model"; - import { PapiFrontendProjectDataProviderService } from "shared/services/project-data-provider.service"; - import { ISettingsService } from "shared/services/settings.service-model"; - import { DialogService } from "shared/services/dialog.service-model"; - import * as papiReact from "renderer/services/papi-frontend-react.service"; - import PapiRendererWebSocket from "renderer/services/renderer-web-socket.service"; - import { IMenuDataService } from "shared/services/menu-data.service-model"; - import PapiRendererXMLHttpRequest from "renderer/services/renderer-xml-http-request.service"; - const papi: { - /** This is just an alias for internet.fetch */ - fetch: typeof globalThis.fetch; - /** JSDOC DESTINATION PapiRendererWebSocket */ - WebSocket: typeof PapiRendererWebSocket; - /** JSDOC DESTINATION PapiRendererXMLHttpRequest */ - XMLHttpRequest: typeof PapiRendererXMLHttpRequest; - /** JSDOC DESTINATION commandService */ - commands: typeof commandService; - /** JSDOC DESTINATION papiWebViewService */ - webViews: WebViewServiceType; - /** JSDOC DESTINATION dialogService */ - dialogs: DialogService; - /** JSDOC DESTINATION papiNetworkService */ - network: PapiNetworkService; - /** JSDOC DESTINATION logger */ - logger: import("electron-log").MainLogger & { - default: import("electron-log").MainLogger; - }; - /** JSDOC DESTINATION internetService */ - internet: InternetService; - /** JSDOC DESTINATION dataProviderService */ - dataProviders: DataProviderService; - /** JSDOC DESTINATION papiFrontendProjectDataProviderService */ - projectDataProviders: PapiFrontendProjectDataProviderService; - /** JSDOC DESTINATION projectLookupService */ - projectLookup: ProjectLookupServiceType; - /** - * JSDOC SOURCE papiReact - * - * React hooks that enable interacting with the `papi` in React components more easily. - */ - react: typeof papiReact; - /** JSDOC DESTINATION settingsService */ - settings: ISettingsService; - /** JSDOC DESTINATION menuDataService */ - menuData: IMenuDataService; - }; - export default papi; - /** This is just an alias for internet.fetch */ - export const fetch: typeof globalThis.fetch; - /** JSDOC DESTINATION PapiRendererWebSocket */ - export const WebSocket: typeof PapiRendererWebSocket; - /** JSDOC DESTINATION PapiRendererXMLHttpRequest */ - export const XMLHttpRequest: typeof PapiRendererXMLHttpRequest; - /** JSDOC DESTINATION commandService */ - export const commands: typeof commandService; - /** JSDOC DESTINATION papiWebViewService */ - export const webViews: WebViewServiceType; - /** JSDOC DESTINATION dialogService */ - export const dialogs: DialogService; - /** JSDOC DESTINATION papiNetworkService */ - export const network: PapiNetworkService; - /** JSDOC DESTINATION logger */ - export const logger: import("electron-log").MainLogger & { - default: import("electron-log").MainLogger; - }; - /** JSDOC DESTINATION internetService */ - export const internet: InternetService; - /** JSDOC DESTINATION dataProviderService */ - export const dataProviders: DataProviderService; - /** JSDOC DESTINATION papiBackendProjectDataProviderService */ - export const projectDataProviders: PapiFrontendProjectDataProviderService; - /** JSDOC DESTINATION projectLookupService */ - export const projectLookup: ProjectLookupServiceType; - /** JSDOC DESTINATION papiReact */ - export const react: typeof papiReact; - /** JSDOC DESTINATION settingsService */ - export const settings: ISettingsService; - /** JSDOC DESTINATION menuDataService */ - export const menuData: IMenuDataService; - export type Papi = typeof papi; + menuData: IMenuDataService; + }; + export default papi; + /** This is just an alias for internet.fetch */ + export const fetch: typeof globalThis.fetch; + /** This wraps the browser's WebSocket implementation to provide + * better control over internet access. It is isomorphic with the standard WebSocket, so it should + * act as a drop-in replacement. + * + * Note that the Node WebSocket implementation is different and not wrapped here. + */ + export const WebSocket: typeof PapiRendererWebSocket; + /** This wraps the browser's XMLHttpRequest implementation to + * provide better control over internet access. It is isomorphic with the standard XMLHttpRequest, + * so it should act as a drop-in replacement. + * + * Note that Node doesn't have a native implementation, so this is only for the renderer. + */ + export const XMLHttpRequest: typeof PapiRendererXMLHttpRequest; + /** + * + * The command service allows you to exchange messages with other components in the platform. You + * can register a command that other services and extensions can send you. You can send commands to + * other services and extensions that have registered commands. + */ + export const commands: typeof commandService; + /** + * + * Service exposing various functions related to using webViews + * + * WebViews are iframes in the Platform.Bible UI into which extensions load frontend code, either + * HTML or React components. + */ + export const webViews: WebViewServiceType; + /** + * + * Prompt the user for responses with dialogs + */ + export const dialogs: DialogService; + /** + * + * Service that provides a way to send and receive network events + */ + export const network: PapiNetworkService; + /** + * + * All extensions and services should use this logger to provide a unified output of logs + */ + export const logger: import('electron-log').MainLogger & { + default: import('electron-log').MainLogger; + }; + /** + * + * Service that provides a way to call `fetch` since the original function is not available + */ + export const internet: InternetService; + /** + * + * Service that allows extensions to send and receive data to/from other extensions + */ + export const dataProviders: DataProviderService; + /** + * + * Service that registers and gets project data providers + */ + export const projectDataProviders: PapiFrontendProjectDataProviderService; + /** + * + * Provides metadata for projects known by the platform + */ + export const projectLookup: ProjectLookupServiceType; + /** + * + * React hooks that enable interacting with the `papi` in React components more easily. + */ + export const react: typeof papiReact; + /** */ + export const settings: ISettingsService; + /** + * + * Service that allows to get and store menu data + */ + export const menuData: IMenuDataService; + export type Papi = typeof papi; } diff --git a/src/renderer/hooks/papi-hooks/use-setting.hook.ts b/src/renderer/hooks/papi-hooks/use-setting.hook.ts index 3ad0b8bb87..6e9a06e750 100644 --- a/src/renderer/hooks/papi-hooks/use-setting.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-setting.hook.ts @@ -56,18 +56,23 @@ const useSetting = ( const defaultStateRef = useRef(defaultState); defaultStateRef.current = defaultState; - // eslint-disable-next-line no-type-assertion/no-type-assertion - const [setting, setSetting] = useData(settingsService)['']( - key, - defaultState, - subscriberOptions, - ) as [ - setting: SettingTypes[SettingName], - setSetting: ( - newData: SettingTypes[SettingName], - ) => Promise>, - boolean, - ]; + /* eslint-disable no-type-assertion/no-type-assertion */ + const [setting, setSetting] = ( + useData(settingsService) as { + ['']: ( + selector: SettingName, + defaultValue: SettingTypes[SettingName], + subscriberOptions?: DataProviderSubscriberOptions, + ) => [ + setting: SettingTypes[SettingName], + setSetting: ( + newData: SettingTypes[SettingName], + ) => Promise>, + boolean, + ]; + } + )[''](key, defaultState, subscriberOptions); + /* eslint-enable */ const resetSetting = useCallback(() => { settingsService.reset(key); From 2439c5ac406e2ba7226e731233bb9f4c9ff3eedf Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Wed, 7 Feb 2024 11:46:25 -0500 Subject: [PATCH 07/19] Processed review comments. Add start service --- lib/papi-dts/papi.d.ts | 23 +++------ src/main/main.ts | 3 ++ src/main/services/settings.service-host.ts | 50 ++----------------- .../run-basic-checks-tab.component.tsx | 24 ++++----- src/shared/services/papi-core.service.ts | 3 -- src/shared/services/settings.service-model.ts | 23 ++------- 6 files changed, 28 insertions(+), 98 deletions(-) diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index e3675a1e7c..2487717dea 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -4128,21 +4128,19 @@ declare module 'extension-host/extension-types/extension-manifest.model' { } declare module 'shared/services/settings.service-model' { import { SettingNames, SettingTypes } from 'papi-shared-types'; - import { OnDidDispose, PlatformEventEmitter, Unsubscriber } from 'platform-bible-utils'; + import { OnDidDispose, Unsubscriber } from 'platform-bible-utils'; import { DataProviderUpdateInstructions, IDataProvider } from '@papi/core'; /** * - * Name used to register the data provider - * - * You can use this name + * This name is used to register the settings service data provider on the papi. You can use this + * name to find the data provider when accessing it using the useData hook */ export const settingsServiceDataProviderName = 'platform.settingsServiceDataProvider'; export const settingsServiceObjectToProxy: Readonly<{ /** * - * Name used to register the data provider - * - * You can use this name + * This name is used to register the settings service data provider on the papi. You can use this + * name to find the data provider when accessing it using the useData hook */ dataProviderName: 'platform.settingsServiceDataProvider'; }>; @@ -4172,15 +4170,6 @@ declare module 'shared/services/settings.service-model' { export type ResetSettingEvent = { type: 'reset-setting'; }; - /** All supported setting events */ - export type SettingEvent = - | UpdateSettingEvent - | ResetSettingEvent; - /** All message subscriptions - emitters that emit an event each time a setting is updated */ - export const onDidUpdateSettingEmitters: Map< - keyof SettingTypes, - PlatformEventEmitter> - >; /** */ export type ISettingsService = { /** @@ -4221,7 +4210,7 @@ declare module 'shared/services/settings.service-model' { */ subscribe( key: SettingName, - callback: (newSetting: SettingEvent) => void, + callback: (newSetting: SettingTypes[SettingName]) => void, ): Promise; } & OnDidDispose & IDataProvider & diff --git a/src/main/main.ts b/src/main/main.ts index 44ac6b8aa2..050f30a148 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -27,6 +27,7 @@ import { get } from '@shared/services/project-data-provider.service'; import { VerseRef } from '@sillsdev/scripture'; import { startNetworkObjectStatusService } from './services/network-object-status.service-host'; import { startLocalizationService } from './services/localization.service-host'; +import { initialize as initializeSettingsService } from './services/settings.service-host'; const PROCESS_CLOSE_TIME_OUT = 2000; @@ -82,6 +83,8 @@ async function main() { await startLocalizationService(); + await initializeSettingsService(); + // TODO (maybe): Wait for signal from the extension host process that it is ready (except 'getWebView') // We could then wait for the renderer to be ready and signal the extension host diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index 81e2ed101f..819608b03d 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -3,24 +3,13 @@ import { DataProviderUpdateInstructions } from '@shared/models/data-provider.mod import dataProviderService, { DataProviderEngine } from '@shared/services/data-provider.service'; import { ISettingsService, - ResetSettingEvent, SettingDataTypes, - SettingEvent, - UpdateSettingEvent, - onDidUpdateSettingEmitters, settingsServiceDataProviderName, settingsServiceObjectToProxy, } from '@shared/services/settings.service-model'; import coreSettingsInfo, { AllSettingsInfo } from '@main/data/core-settings-info.data'; import { SettingNames, SettingTypes } from 'papi-shared-types'; -import { - createSyncProxyForAsyncObject, - PlatformEventEmitter, - // UnsubscriberAsync, - deserialize, - serialize, - Unsubscriber, -} from 'platform-bible-utils'; +import { createSyncProxyForAsyncObject, deserialize, serialize } from 'platform-bible-utils'; // TODO: 4 Fix implementation of all functions // TODO: Where do settings live (JSON obj/file)? How is dp going to access it? @@ -56,49 +45,16 @@ class SettingDataProviderEngine newSetting: SettingTypes[SettingName], ): Promise> { localStorage.setItem(key, serialize(newSetting)); - // Assert type of the particular SettingName of the emitter. - // eslint-disable-next-line no-type-assertion/no-type-assertion - const emitter = onDidUpdateSettingEmitters.get(key); - const setMessage: UpdateSettingEvent = { - setting: newSetting, - type: 'update-setting', - }; - emitter?.emit(setMessage); + this.notifyUpdate('*'); return true; } // eslint-disable-next-line class-methods-use-this async reset(key: SettingName): Promise { localStorage.removeItem(key); - // Assert type of the particular SettingName of the emitter. - // eslint-disable-next-line no-type-assertion/no-type-assertion - const emitter = onDidUpdateSettingEmitters.get(key); - const resetMessage: ResetSettingEvent = { type: 'reset-setting' }; - emitter?.emit(resetMessage); + this.notifyUpdate('*'); return true; } - - // eslint-disable-next-line class-methods-use-this - async subscribe( - key: SettingName, - callback: (newSetting: SettingEvent) => void, - ): Promise { - // Assert type of the particular SettingName of the emitter. - // eslint-disable-next-line no-type-assertion/no-type-assertion - let emitter = onDidUpdateSettingEmitters.get(key) as - | PlatformEventEmitter> - | undefined; - if (!emitter) { - emitter = new PlatformEventEmitter>(); - onDidUpdateSettingEmitters.set( - key, - // Assert type of the general SettingNames of the emitter. - // eslint-disable-next-line no-type-assertion/no-type-assertion - emitter as PlatformEventEmitter>, - ); - } - return emitter.subscribe(callback); - } } let initializationPromise: Promise; diff --git a/src/renderer/components/run-basic-checks-dialog/run-basic-checks-tab.component.tsx b/src/renderer/components/run-basic-checks-dialog/run-basic-checks-tab.component.tsx index b577830bde..8f906f2db4 100644 --- a/src/renderer/components/run-basic-checks-dialog/run-basic-checks-tab.component.tsx +++ b/src/renderer/components/run-basic-checks-dialog/run-basic-checks-tab.component.tsx @@ -4,7 +4,6 @@ import { getChaptersForBook } from 'platform-bible-utils'; import logger from '@shared/services/logger.service'; import { Typography } from '@mui/material'; import { useState, useMemo } from 'react'; -import settingsService from '@shared/services/settings.service'; import BookSelector from '@renderer/components/run-basic-checks-dialog/book-selector.component'; import BasicChecks, { fetchChecks, @@ -12,20 +11,24 @@ import BasicChecks, { import './run-basic-checks-tab.component.scss'; import useProjectDataProvider from '@renderer/hooks/papi-hooks/use-project-data-provider.hook'; import { VerseRef } from '@sillsdev/scripture'; +import useSetting from '@renderer/hooks/papi-hooks/use-setting.hook'; export const TAB_TYPE_RUN_BASIC_CHECKS = 'run-basic-checks'; -// Changing global scripture reference won't effect the dialog because reference is passed in once at the start. type RunBasicChecksTabProps = { - currentScriptureReference: ScriptureReference | undefined; currentProjectId: string | undefined; }; -export default function RunBasicChecksTab({ - currentScriptureReference, - currentProjectId, -}: RunBasicChecksTabProps) { - const currentBookNumber = currentScriptureReference?.bookNum ?? 1; +const defaultScrRef: ScriptureReference = { + bookNum: 1, + chapterNum: 1, + verseNum: 1, +}; + +export default function RunBasicChecksTab({ currentProjectId }: RunBasicChecksTabProps) { + const [scrRef] = useSetting('platform.verseRef', defaultScrRef); + + const currentBookNumber = scrRef?.bookNum ?? 1; const basicChecks = fetchChecks(); // used within chapter-range-selector and won't change because current book doesn't change @@ -138,11 +141,6 @@ export const loadRunBasicChecksTab = (savedTabInfo: SavedTabInfo): TabInfo => { ), }; diff --git a/src/shared/services/papi-core.service.ts b/src/shared/services/papi-core.service.ts index 9d68fc61a9..9307b645ce 100644 --- a/src/shared/services/papi-core.service.ts +++ b/src/shared/services/papi-core.service.ts @@ -33,6 +33,3 @@ export type { } from '@shared/models/web-view.model'; export type { IWebViewProvider } from '@shared/models/web-view-provider.model'; - -// TODO: Do we need this here? How to fix? -// export type { SettingEvent } from '@shared/services/settings.service-model'; diff --git a/src/shared/services/settings.service-model.ts b/src/shared/services/settings.service-model.ts index fa7fc1e895..0a8430802d 100644 --- a/src/shared/services/settings.service-model.ts +++ b/src/shared/services/settings.service-model.ts @@ -1,21 +1,19 @@ import { SettingNames, SettingTypes } from 'papi-shared-types'; import { OnDidDispose, - PlatformEventEmitter, Unsubscriber, // UnsubscriberAsync, } from 'platform-bible-utils'; import { DataProviderUpdateInstructions, IDataProvider } from './papi-core.service'; -/** JSDOC DESTINATION dataProviderName */ +/** JSDOC DESTINATION settingsServiceDataProviderName */ export const settingsServiceDataProviderName = 'platform.settingsServiceDataProvider'; export const settingsServiceObjectToProxy = Object.freeze({ /** - * JSDOC SOURCE dataProviderName + * JSDOC SOURCE settingsServiceDataProviderName * - * Name used to register the data provider - * - * You can use this name + * This name is used to register the settings service data provider on the papi. You can use this + * name to find the data provider when accessing it using the useData hook */ dataProviderName: settingsServiceDataProviderName, }); @@ -51,17 +49,6 @@ export type ResetSettingEvent = { type: 'reset-setting'; }; -/** All supported setting events */ -export type SettingEvent = - | UpdateSettingEvent - | ResetSettingEvent; - -/** All message subscriptions - emitters that emit an event each time a setting is updated */ -export const onDidUpdateSettingEmitters = new Map< - SettingNames, - PlatformEventEmitter> ->(); - /** JSDOC SOURCE settingsService */ export type ISettingsService = { /** @@ -105,7 +92,7 @@ export type ISettingsService = { */ subscribe( key: SettingName, - callback: (newSetting: SettingEvent) => void, + callback: (newSetting: SettingTypes[SettingName]) => void, ): Promise; } & OnDidDispose & IDataProvider & From 9ef5ab8af7be147b42f204be6f7b629e8fa8af2d Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Wed, 7 Feb 2024 15:10:38 -0500 Subject: [PATCH 08/19] Add loading settings from file --- src/main/services/settings.service-host.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index 819608b03d..89610b2e62 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -10,6 +10,20 @@ import { import coreSettingsInfo, { AllSettingsInfo } from '@main/data/core-settings-info.data'; import { SettingNames, SettingTypes } from 'papi-shared-types'; import { createSyncProxyForAsyncObject, deserialize, serialize } from 'platform-bible-utils'; +import { joinUriPaths } from '@node/utils/util'; +import * as nodeFS from '@node/services/node-file-system.service'; + +const SETTINGS_FILE_URI = joinUriPaths('data://', 'settings.json'); + +let settingData = new Map(); + +async function loadSettingsFromFile() { + settingData.clear(); + const settingFileString = await nodeFS.readFileText(SETTINGS_FILE_URI); + settingData = deserialize(settingFileString); + if (typeof settingData !== 'object') + throw new Error(`Settings data located in '${SETTINGS_FILE_URI}' is invalid`); +} // TODO: 4 Fix implementation of all functions // TODO: Where do settings live (JSON obj/file)? How is dp going to access it? @@ -69,6 +83,7 @@ export async function initialize(): Promise { settingsServiceDataProviderName, new SettingDataProviderEngine(coreSettingsInfo), // will be fixed when dp types are correct ); + loadSettingsFromFile(); resolve(); } catch (error) { reject(error); From fb9ba84fb2a9c7ce682c48a49cb83d65d134a6c3 Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Thu, 8 Feb 2024 14:47:07 -0500 Subject: [PATCH 09/19] Process review comments --- lib/papi-dts/papi.d.ts | 216 +++++++++--------- .../services/papi-backend.service.ts | 4 + src/main/services/settings.service-host.ts | 4 +- .../hooks/papi-hooks/use-setting.hook.ts | 28 +-- src/shared/services/settings.service-model.ts | 21 +- 5 files changed, 144 insertions(+), 129 deletions(-) diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index 2487717dea..cfb6a60f75 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -3881,6 +3881,108 @@ declare module 'shared/services/menu-data.service' { const menuDataService: IMenuDataService; export default menuDataService; } +declare module 'shared/services/settings.service-model' { + import { SettingNames, SettingTypes } from 'papi-shared-types'; + import { OnDidDispose, Unsubscriber } from 'platform-bible-utils'; + import { + DataProviderSubscriberOptions, + DataProviderUpdateInstructions, + IDataProvider, + } from '@papi/core'; + /** + * + * This name is used to register the settings service data provider on the papi. You can use this + * name to find the data provider when accessing it using the useData hook + */ + export const settingsServiceDataProviderName = 'platform.settingsServiceDataProvider'; + export const settingsServiceObjectToProxy: Readonly<{ + /** + * + * This name is used to register the settings service data provider on the papi. You can use this + * name to find the data provider when accessing it using the useData hook + */ + dataProviderName: 'platform.settingsServiceDataProvider'; + }>; + /** + * SettingDataTypes handles getting and setting Platform.Bible core application and extension + * settings. + * + * Note: the unnamed (`''`) data type is not actually part of `SettingDataTypes` because the methods + * would not be able to create a generic type extending from `SettingNames` in order to return the + * specific setting type being requested. As such, `get`, `set`, `reset` and `subscribe` are all + * specified on {@link ISettingsService} instead. Unfortunately, as a result, using Intellisense with + * `useData` will not show the unnamed data type (`''`) as an option, but you can use `useSetting` + * instead. However, do note that the unnamed data type (`''`) is fully functional. + */ + export type SettingDataTypes = {}; + module 'papi-shared-types' { + interface DataProviders { + [settingsServiceDataProviderName]: ISettingsService; + } + } + /** Event to set or update a setting */ + export type UpdateSettingEvent = { + type: 'update-setting'; + setting: SettingTypes[SettingName]; + }; + /** Event to remove a setting */ + export type ResetSettingEvent = { + type: 'reset-setting'; + }; + /** */ + export type ISettingsService = { + /** + * Retrieves the value of the specified setting + * + * @param key The string id of the setting for which the value is being retrieved + * @returns The value of the specified setting, parsed to an object. Returns default setting if + * setting does not exist + * @throws If no default value is available for the setting. + */ + get(key: SettingName): Promise; + /** + * Sets the value of the specified setting + * + * @param key The string id of the setting for which the value is being set + * @param newSetting The value that is to be set for the specified key + * @returns Information that papi uses to interpret whether to send out updates. Defaults to + * `true` (meaning send updates only for this data type). + * @see {@link DataProviderUpdateInstructions} for more info on what to return + */ + set( + key: SettingName, + newSetting: SettingTypes[SettingName], + ): Promise>; + /** + * Removes the setting from memory and resets it to its default value + * + * @param key The string id of the setting for which the value is being removed + * @returns `true` if successfully reset the project setting. `false` otherwise + */ + reset(key: SettingName): Promise; + /** + * Subscribes to updates of the specified setting. Whenever the value of the setting changes, the + * callback function is executed. + * + * @param key The string id of the setting for which the value is being subscribed to + * @param callback The function that will be called whenever the specified setting is updated + * @param options Various options to adjust how the subscriber emits updates + * @returns Unsubscriber that should be called whenever the subscription should be deleted + */ + subscribe( + key: SettingName, + callback: (newSetting: SettingTypes[SettingName]) => void, + options: DataProviderSubscriberOptions, + ): Promise; + } & OnDidDispose & + IDataProvider & + typeof settingsServiceObjectToProxy; +} +declare module 'shared/services/settings.service' { + import { ISettingsService } from 'shared/services/settings.service-model'; + const settingsService: ISettingsService; + export default settingsService; +} declare module '@papi/backend' { /** * Unified module for accessing API features in the extension host. @@ -3901,6 +4003,7 @@ declare module '@papi/backend' { import { ProjectLookupServiceType } from 'shared/services/project-lookup.service-model'; import { DialogService } from 'shared/services/dialog.service-model'; import { IMenuDataService } from 'shared/services/menu-data.service-model'; + import { ISettingsService } from 'shared/services/settings.service-model'; const papi: { /** * @@ -3977,6 +4080,8 @@ declare module '@papi/backend' { * within the renderer. */ storage: ExtensionStorageService; + /** */ + settings: ISettingsService; /** * * Service that allows to get and store menu data @@ -4126,101 +4231,6 @@ declare module 'extension-host/extension-types/extension-manifest.model' { activationEvents: string[]; }; } -declare module 'shared/services/settings.service-model' { - import { SettingNames, SettingTypes } from 'papi-shared-types'; - import { OnDidDispose, Unsubscriber } from 'platform-bible-utils'; - import { DataProviderUpdateInstructions, IDataProvider } from '@papi/core'; - /** - * - * This name is used to register the settings service data provider on the papi. You can use this - * name to find the data provider when accessing it using the useData hook - */ - export const settingsServiceDataProviderName = 'platform.settingsServiceDataProvider'; - export const settingsServiceObjectToProxy: Readonly<{ - /** - * - * This name is used to register the settings service data provider on the papi. You can use this - * name to find the data provider when accessing it using the useData hook - */ - dataProviderName: 'platform.settingsServiceDataProvider'; - }>; - /** - * SettingDataTypes handles getting and setting Platform.Bible core application and extension - * settings. - * - * Note: the unnamed (`''`) data type is not actually part of `SettingDataTypes` because the methods - * would not be able to create a generic type extending from `SettingNames` in order to return the - * specific setting type being requested. As such, `get`, `set`, `reset` and `subscribe` are all - * specified on {@link ISettingsService} instead. Unfortunately, as a result, using Intellisense with - * `useData` will not show the unnamed data type (`''`) as an option, but you can use `useSetting` - * instead. However, do note that the unnamed data type (`''`) is fully functional. - */ - export type SettingDataTypes = {}; - module 'papi-shared-types' { - interface DataProviders { - [settingsServiceDataProviderName]: ISettingsService; - } - } - /** Event to set or update a setting */ - export type UpdateSettingEvent = { - type: 'update-setting'; - setting: SettingTypes[SettingName]; - }; - /** Event to remove a setting */ - export type ResetSettingEvent = { - type: 'reset-setting'; - }; - /** */ - export type ISettingsService = { - /** - * Retrieves the value of the specified setting - * - * @param key The string id of the setting for which the value is being retrieved - * @param defaultSetting The default value used for the setting if no value is available for the - * key - * @returns The value of the specified setting, parsed to an object. Returns default setting if - * setting does not exist - */ - get(key: SettingName): Promise; - /** - * Sets the value of the specified setting - * - * @param key The string id of the setting for which the value is being retrieved - * @param newSetting The value that is to be stored. Setting the new value to `undefined` is the - * equivalent of deleting the setting - */ - set( - key: SettingName, - newSetting: SettingTypes[SettingName], - ): Promise>; - /** - * Removes the setting from memory - * - * @param key The string id of the setting for which the value is being removed - * @returns `true` if successfully reset the project setting. `false` otherwise - */ - reset(key: SettingName): Promise; - /** - * Subscribes to updates of the specified setting. Whenever the value of the setting changes, the - * callback function is executed. - * - * @param key The string id of the setting for which the value is being subscribed to - * @param callback The function that will be called whenever the specified setting is updated - * @returns Unsubscriber that should be called whenever the subscription should be deleted - */ - subscribe( - key: SettingName, - callback: (newSetting: SettingTypes[SettingName]) => void, - ): Promise; - } & OnDidDispose & - IDataProvider & - typeof settingsServiceObjectToProxy; -} -declare module 'shared/services/settings.service' { - import { ISettingsService } from 'shared/services/settings.service-model'; - const settingsService: ISettingsService; - export default settingsService; -} declare module 'renderer/hooks/hook-generators/create-use-network-object-hook.util' { import { NetworkObject } from 'shared/models/network-object.model'; /** @@ -4440,21 +4450,18 @@ declare module 'renderer/hooks/papi-hooks/use-setting.hook' { import { SettingDataTypes } from 'shared/services/settings.service-model'; /** * Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes - * and gets updated when the setting is changed by others. + * and gets updated when the setting is changed by others. Running `resetSetting()` will always + * update the setting value returned to the latest `defaultState`, and changing the `key` will use + * the latest `defaultState`. However, if `defaultState` is changed while a setting is + * `defaultState` (meaning it is reset and has no value), the returned setting value will not be + * updated to the new `defaultState`. * * @param key The string id that is used to store the setting in local storage * * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be * updated every render * @param defaultState The default state of the setting. If the setting already has a value set to - * it in local storage, this parameter will be ignored. - * - * Note: this parameter is internally assigned to a `ref`, so changing it will not cause any hooks - * to re-run with its new value. Running `resetSetting()` will always update the setting value - * returned to the latest `defaultState`, and changing the `key` will use the latest - * `defaultState`. However, if `defaultState` is changed while a setting is `defaultState` - * (meaning it is reset and has no value), the returned setting value will not be updated to the - * new `defaultState`. + * it in the settings storage, this parameter will be ignored. * @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 @@ -4481,6 +4488,7 @@ declare module 'renderer/hooks/papi-hooks/use-setting.hook' { newData: SettingTypes[SettingName], ) => Promise>, resetSetting: () => void, + isLoading: boolean, ]; export default useSetting; } diff --git a/src/extension-host/services/papi-backend.service.ts b/src/extension-host/services/papi-backend.service.ts index 9a72917f2f..88f32c4119 100644 --- a/src/extension-host/services/papi-backend.service.ts +++ b/src/extension-host/services/papi-backend.service.ts @@ -31,6 +31,8 @@ import dialogService from '@shared/services/dialog.service'; import { DialogService } from '@shared/services/dialog.service-model'; import menuDataService from '@shared/services/menu-data.service'; import { IMenuDataService } from '@shared/services/menu-data.service-model'; +import settingsService from '@shared/services/settings.service'; +import { ISettingsService } from '@shared/services/settings.service-model'; // IMPORTANT NOTES: // 1) When adding new services here, consider whether they also belong in papi-frontend.service.ts. @@ -73,6 +75,8 @@ const papi = { projectLookup: projectLookupService as ProjectLookupServiceType, /** JSDOC DESTINATION extensionStorageService */ storage: extensionStorageService as ExtensionStorageService, + /** JSDOC DESTINATION settingsService */ + settings: settingsService as ISettingsService, /** JSDOC DESTINATION menuDataService */ menuData: menuDataService as IMenuDataService, }; diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index 89610b2e62..4b3009dc4d 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -59,14 +59,14 @@ class SettingDataProviderEngine newSetting: SettingTypes[SettingName], ): Promise> { localStorage.setItem(key, serialize(newSetting)); - this.notifyUpdate('*'); return true; } // eslint-disable-next-line class-methods-use-this async reset(key: SettingName): Promise { localStorage.removeItem(key); - this.notifyUpdate('*'); + // this.notifyUpdate(''); TODO: Fix + // TODO: Add return true if successfully reset, return false otherwise return true; } } diff --git a/src/renderer/hooks/papi-hooks/use-setting.hook.ts b/src/renderer/hooks/papi-hooks/use-setting.hook.ts index 6e9a06e750..75d3e810d3 100644 --- a/src/renderer/hooks/papi-hooks/use-setting.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-setting.hook.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef } from 'react'; +import { useCallback } from 'react'; import settingsService from '@shared/services/settings.service'; import { SettingNames, SettingTypes } from 'papi-shared-types'; import useData from '@renderer/hooks/papi-hooks/use-data.hook'; @@ -10,21 +10,18 @@ import { SettingDataTypes } from '@shared/services/settings.service-model'; /** * Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes - * and gets updated when the setting is changed by others. + * and gets updated when the setting is changed by others. Running `resetSetting()` will always + * update the setting value returned to the latest `defaultState`, and changing the `key` will use + * the latest `defaultState`. However, if `defaultState` is changed while a setting is + * `defaultState` (meaning it is reset and has no value), the returned setting value will not be + * updated to the new `defaultState`. * * @param key The string id that is used to store the setting in local storage * * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be * updated every render * @param defaultState The default state of the setting. If the setting already has a value set to - * it in local storage, this parameter will be ignored. - * - * Note: this parameter is internally assigned to a `ref`, so changing it will not cause any hooks - * to re-run with its new value. Running `resetSetting()` will always update the setting value - * returned to the latest `defaultState`, and changing the `key` will use the latest - * `defaultState`. However, if `defaultState` is changed while a setting is `defaultState` - * (meaning it is reset and has no value), the returned setting value will not be updated to the - * new `defaultState`. + * it in the settings storage, this parameter will be ignored. * @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 @@ -51,13 +48,12 @@ const useSetting = ( newData: SettingTypes[SettingName], ) => Promise>, resetSetting: () => void, + isLoading: boolean, ] => { - // Use defaultState as a ref so it doesn't update dependency arrays - const defaultStateRef = useRef(defaultState); - defaultStateRef.current = defaultState; - + // Since the `DataProviderDataType` that we're trying to expose here is unnamed (`''`) we have to + // manually assert it's signature in order for useData to know how to work with this data provider. /* eslint-disable no-type-assertion/no-type-assertion */ - const [setting, setSetting] = ( + const [setting, setSetting, isLoading] = ( useData(settingsService) as { ['']: ( selector: SettingName, @@ -78,6 +74,6 @@ const useSetting = ( settingsService.reset(key); }, [key]); - return [setting, setSetting, resetSetting]; + return [setting, setSetting, resetSetting, isLoading]; }; export default useSetting; diff --git a/src/shared/services/settings.service-model.ts b/src/shared/services/settings.service-model.ts index 0a8430802d..130286094e 100644 --- a/src/shared/services/settings.service-model.ts +++ b/src/shared/services/settings.service-model.ts @@ -4,7 +4,11 @@ import { Unsubscriber, // UnsubscriberAsync, } from 'platform-bible-utils'; -import { DataProviderUpdateInstructions, IDataProvider } from './papi-core.service'; +import { + DataProviderSubscriberOptions, + DataProviderUpdateInstructions, + IDataProvider, +} from './papi-core.service'; /** JSDOC DESTINATION settingsServiceDataProviderName */ export const settingsServiceDataProviderName = 'platform.settingsServiceDataProvider'; @@ -55,19 +59,20 @@ export type ISettingsService = { * Retrieves the value of the specified setting * * @param key The string id of the setting for which the value is being retrieved - * @param defaultSetting The default value used for the setting if no value is available for the - * key * @returns The value of the specified setting, parsed to an object. Returns default setting if * setting does not exist + * @throws If no default value is available for the setting. */ get(key: SettingName): Promise; /** * Sets the value of the specified setting * - * @param key The string id of the setting for which the value is being retrieved - * @param newSetting The value that is to be stored. Setting the new value to `undefined` is the - * equivalent of deleting the setting + * @param key The string id of the setting for which the value is being set + * @param newSetting The value that is to be set for the specified key + * @returns Information that papi uses to interpret whether to send out updates. Defaults to + * `true` (meaning send updates only for this data type). + * @see {@link DataProviderUpdateInstructions} for more info on what to return */ set( key: SettingName, @@ -75,7 +80,7 @@ export type ISettingsService = { ): Promise>; /** - * Removes the setting from memory + * Removes the setting from memory and resets it to its default value * * @param key The string id of the setting for which the value is being removed * @returns `true` if successfully reset the project setting. `false` otherwise @@ -88,11 +93,13 @@ export type ISettingsService = { * * @param key The string id of the setting for which the value is being subscribed to * @param callback The function that will be called whenever the specified setting is updated + * @param options Various options to adjust how the subscriber emits updates * @returns Unsubscriber that should be called whenever the subscription should be deleted */ subscribe( key: SettingName, callback: (newSetting: SettingTypes[SettingName]) => void, + options: DataProviderSubscriberOptions, ): Promise; } & OnDidDispose & IDataProvider & From 6633969ce334d72db02babd67cf4319a39dd9345 Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Fri, 9 Feb 2024 11:11:30 -0500 Subject: [PATCH 10/19] Process more review comments --- lib/papi-dts/papi.d.ts | 21 +++---------------- .../hooks/papi-hooks/use-setting.hook.ts | 17 ++++++--------- src/shared/services/data-provider.service.ts | 8 ++++++- src/shared/services/settings.service-model.ts | 11 ---------- 4 files changed, 16 insertions(+), 41 deletions(-) diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index cfb6a60f75..ff40d2ac96 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -3920,15 +3920,6 @@ declare module 'shared/services/settings.service-model' { [settingsServiceDataProviderName]: ISettingsService; } } - /** Event to set or update a setting */ - export type UpdateSettingEvent = { - type: 'update-setting'; - setting: SettingTypes[SettingName]; - }; - /** Event to remove a setting */ - export type ResetSettingEvent = { - type: 'reset-setting'; - }; /** */ export type ISettingsService = { /** @@ -4449,19 +4440,13 @@ declare module 'renderer/hooks/papi-hooks/use-setting.hook' { } from 'shared/models/data-provider.model'; import { SettingDataTypes } from 'shared/services/settings.service-model'; /** - * Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes - * and gets updated when the setting is changed by others. Running `resetSetting()` will always - * update the setting value returned to the latest `defaultState`, and changing the `key` will use - * the latest `defaultState`. However, if `defaultState` is changed while a setting is - * `defaultState` (meaning it is reset and has no value), the returned setting value will not be - * updated to the new `defaultState`. + * Gets, sets and resets a setting on the papi * - * @param key The string id that is used to store the setting in local storage + * @param key The string id that is used to identify the setting that will be stored on the papi * * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be * updated every render - * @param defaultState The default state of the setting. If the setting already has a value set to - * it in the settings storage, this parameter will be ignored. + * @param defaultState The initial value to return while first awaiting the setting value * @param subscriberOptions Various options to adjust how the subscriber emits updates * * Note: this parameter is internally assigned to a `ref`, so changing it will not cause any hooks diff --git a/src/renderer/hooks/papi-hooks/use-setting.hook.ts b/src/renderer/hooks/papi-hooks/use-setting.hook.ts index 75d3e810d3..8e0ab4904a 100644 --- a/src/renderer/hooks/papi-hooks/use-setting.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-setting.hook.ts @@ -9,19 +9,13 @@ import { import { SettingDataTypes } from '@shared/services/settings.service-model'; /** - * Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes - * and gets updated when the setting is changed by others. Running `resetSetting()` will always - * update the setting value returned to the latest `defaultState`, and changing the `key` will use - * the latest `defaultState`. However, if `defaultState` is changed while a setting is - * `defaultState` (meaning it is reset and has no value), the returned setting value will not be - * updated to the new `defaultState`. + * Gets, sets and resets a setting on the papi * - * @param key The string id that is used to store the setting in local storage + * @param key The string id that is used to identify the setting that will be stored on the papi * * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be * updated every render - * @param defaultState The default state of the setting. If the setting already has a value set to - * it in the settings storage, this parameter will be ignored. + * @param defaultState The initial value to return while first awaiting the setting value * @param subscriberOptions Various options to adjust how the subscriber emits updates * * Note: this parameter is internally assigned to a `ref`, so changing it will not cause any hooks @@ -50,8 +44,9 @@ const useSetting = ( resetSetting: () => void, isLoading: boolean, ] => { - // Since the `DataProviderDataType` that we're trying to expose here is unnamed (`''`) we have to - // manually assert it's signature in order for useData to know how to work with this data provider. + // `SettingDataTypes` has no data types defined on it. We're using custom methods to interact + // with the data provider. The useData hook is not able to see these, so we are asserting them + // because we know we've defined them on the data provider. /* eslint-disable no-type-assertion/no-type-assertion */ const [setting, setSetting, isLoading] = ( useData(settingsService) as { diff --git a/src/shared/services/data-provider.service.ts b/src/shared/services/data-provider.service.ts index 2962f35082..617b245703 100644 --- a/src/shared/services/data-provider.service.ts +++ b/src/shared/services/data-provider.service.ts @@ -320,9 +320,15 @@ function createDataProviderProxy( // We create `subscribe` and `notifyUpdate` for extensions, and // `subscribe` uses `get` internally, so those 3 properties can't // change after the data provider has been created or bad things will happen. + // Locally the data provider engine has getters and the data provider service creates the + // subscribers and notifyUpdate. + // Remotely this proxy creates subscribers, there is no need for notifyUpdate, and the + // network object service sets getters as network request functions through this proxy. + // These request functions should not have to change after they're set for the first time. if ( isString(prop) && - (prop === 'get' || prop.startsWith('subscribe') || prop === 'notifyUpdate') + (prop.startsWith('get') || prop.startsWith('subscribe') || prop === 'notifyUpdate') && + (prop in obj || prop in dataProviderInternal) ) return false; diff --git a/src/shared/services/settings.service-model.ts b/src/shared/services/settings.service-model.ts index 130286094e..1f0ac2317f 100644 --- a/src/shared/services/settings.service-model.ts +++ b/src/shared/services/settings.service-model.ts @@ -42,17 +42,6 @@ declare module 'papi-shared-types' { } } -/** Event to set or update a setting */ -export type UpdateSettingEvent = { - type: 'update-setting'; - setting: SettingTypes[SettingName]; -}; - -/** Event to remove a setting */ -export type ResetSettingEvent = { - type: 'reset-setting'; -}; - /** JSDOC SOURCE settingsService */ export type ISettingsService = { /** From 388a369a9b3767afdcfd9981799af906723f3e97 Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Fri, 9 Feb 2024 11:12:41 -0500 Subject: [PATCH 11/19] Remove TODOs --- src/main/services/settings.service-host.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index 4b3009dc4d..747544e401 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -25,8 +25,6 @@ async function loadSettingsFromFile() { throw new Error(`Settings data located in '${SETTINGS_FILE_URI}' is invalid`); } -// TODO: 4 Fix implementation of all functions -// TODO: Where do settings live (JSON obj/file)? How is dp going to access it? class SettingDataProviderEngine extends DataProviderEngine implements IDataProviderEngine From 3d48b4746a2536609f73cf502f6b7ada0dd03204 Mon Sep 17 00:00:00 2001 From: Jolie Rabideau Date: Mon, 12 Feb 2024 09:37:18 -0500 Subject: [PATCH 12/19] fix unit test bug, start settings service unit tests, and small fix in model --- src/__tests__/app.component.test.tsx | 4 +++ .../services/settings.service-host.test.tsx | 28 +++++++++++++++++++ src/shared/services/settings.service-model.ts | 10 ++----- 3 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 src/main/services/settings.service-host.test.tsx diff --git a/src/__tests__/app.component.test.tsx b/src/__tests__/app.component.test.tsx index f1335b3423..b0db2cbae2 100644 --- a/src/__tests__/app.component.test.tsx +++ b/src/__tests__/app.component.test.tsx @@ -31,6 +31,10 @@ jest.mock('@renderer/components/docking/platform-dock-layout.component', () => ( __esModule: true, default: /** ParanextDockLayout Mock */ () => undefined, })); +jest.mock('@renderer/components/platform-bible-toolbar', () => ({ + __esModule: true, + default: /** PlatformBibleToolbar Mock */ () =>
, +})); describe('App', () => { it('should render', async () => { diff --git a/src/main/services/settings.service-host.test.tsx b/src/main/services/settings.service-host.test.tsx new file mode 100644 index 0000000000..99c30416a1 --- /dev/null +++ b/src/main/services/settings.service-host.test.tsx @@ -0,0 +1,28 @@ +import { testingSettingService } from '@main/services/settings.service-host'; + +const VERSE_REF_DEFAULT = { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } }; +const NEW_VERSE_REF = { bookNum: 2, chapterNum: 2, verseNum: 2 }; +const settingsProviderEngine = testingSettingService.implementSettingDataProviderEngine(); + +jest.mock('@node/services/node-file-system.service', () => ({ + readFileText: () => { + return JSON.stringify(VERSE_REF_DEFAULT); + }, +})); + +// TODO: Update tests when edge cases are defined in functions + +test('Get verseRef returns default value', async () => { + const result = await settingsProviderEngine.get('platform.verseRef'); + expect(result).toEqual(VERSE_REF_DEFAULT.default); +}); + +test('Set verseRef returns true', async () => { + const result = await settingsProviderEngine.set('platform.verseRef', NEW_VERSE_REF); + expect(result).toBe(true); +}); + +test('Reset verseRef returns true', async () => { + const result = await settingsProviderEngine.reset('platform.verseRef'); + expect(result).toBe(true); +}); diff --git a/src/shared/services/settings.service-model.ts b/src/shared/services/settings.service-model.ts index 1f0ac2317f..b629b7209d 100644 --- a/src/shared/services/settings.service-model.ts +++ b/src/shared/services/settings.service-model.ts @@ -1,9 +1,5 @@ import { SettingNames, SettingTypes } from 'papi-shared-types'; -import { - OnDidDispose, - Unsubscriber, - // UnsubscriberAsync, -} from 'platform-bible-utils'; +import { OnDidDispose, UnsubscriberAsync } from 'platform-bible-utils'; import { DataProviderSubscriberOptions, DataProviderUpdateInstructions, @@ -88,8 +84,8 @@ export type ISettingsService = { subscribe( key: SettingName, callback: (newSetting: SettingTypes[SettingName]) => void, - options: DataProviderSubscriberOptions, - ): Promise; + options?: DataProviderSubscriberOptions, + ): Promise; } & OnDidDispose & IDataProvider & typeof settingsServiceObjectToProxy; From f4dc24768756ce640aa341581541eee3d8f2bc13 Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Mon, 12 Feb 2024 13:50:37 -0500 Subject: [PATCH 13/19] Working on getter, setter and reset --- src/declarations/papi-shared-types.ts | 6 +- src/main/data/core-settings-info.data.ts | 1 + .../services/settings.service-host.test.ts | 51 ++++++++++++ .../services/settings.service-host.test.tsx | 28 ------- src/main/services/settings.service-host.ts | 81 ++++++++++++------- .../hooks/papi-hooks/use-setting.hook.ts | 3 +- src/shared/services/settings.service-model.ts | 4 + 7 files changed, 112 insertions(+), 62 deletions(-) create mode 100644 src/main/services/settings.service-host.test.ts delete mode 100644 src/main/services/settings.service-host.test.tsx diff --git a/src/declarations/papi-shared-types.ts b/src/declarations/papi-shared-types.ts index 47d59bf969..bc8555c036 100644 --- a/src/declarations/papi-shared-types.ts +++ b/src/declarations/papi-shared-types.ts @@ -55,11 +55,7 @@ declare module 'papi-shared-types' { export interface SettingTypes { 'platform.verseRef': ScriptureReference; - // 'platform.interfaceLanguage': InterfaceLanguage; - // With only one key in this interface, `papi.d.ts` was baking in the literal string when - // `SettingNames` was being used. Adding a placeholder key makes TypeScript generate `papi.d.ts` - // correctly. When we add another setting, we can remove this placeholder. - placeholder: undefined; + 'platform.interfaceLanguage': string; } export type SettingNames = keyof SettingTypes; diff --git a/src/main/data/core-settings-info.data.ts b/src/main/data/core-settings-info.data.ts index 82b23dcd18..fcd96a5f27 100644 --- a/src/main/data/core-settings-info.data.ts +++ b/src/main/data/core-settings-info.data.ts @@ -13,6 +13,7 @@ export type AllSettingsInfo = { /** Info about all settings built into core. Does not contain info for extensions' settings */ const coreSettingsInfo: Partial = { 'platform.verseRef': { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } }, + 'platform.interfaceLanguage': { default: 'eng' }, }; export default coreSettingsInfo; diff --git a/src/main/services/settings.service-host.test.ts b/src/main/services/settings.service-host.test.ts new file mode 100644 index 0000000000..6c13a864f4 --- /dev/null +++ b/src/main/services/settings.service-host.test.ts @@ -0,0 +1,51 @@ +import { testingSettingService } from '@main/services/settings.service-host'; +import { AllSettingsData } from '@shared/services/settings.service-model'; +import { SettingTypes } from 'papi-shared-types'; + +const PARTIAL_SETTINGS_DATA: Partial = { + 'platform.interfaceLanguage': 'eng', +}; + +const VERSE_REF_DEFAULT = { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } }; +// const NEW_VERSE_REF = { bookNum: 2, chapterNum: 2, verseNum: 2 }; +const INTERFACE_LANGUAGE_DEFAULT = { default: 'eng' }; + +const settingsProviderEngine = + testingSettingService.implementSettingDataProviderEngine(PARTIAL_SETTINGS_DATA); + +jest.mock('@node/services/node-file-system.service', () => ({ + readFileText: () => { + return JSON.stringify(VERSE_REF_DEFAULT); + }, +})); + +// TODO: Update tests when edge cases are defined in functions + +test('Get verseRef returns default value', async () => { + const result = await settingsProviderEngine.get('platform.verseRef'); + expect(result).toEqual(VERSE_REF_DEFAULT.default); +}); + +test('Get verseRef returns stored value', async () => { + const result = await settingsProviderEngine.get('platform.interfaceLanguage'); + expect(result).toEqual(INTERFACE_LANGUAGE_DEFAULT.default); +}); + +test('Key does not exist (on both settings file and list of known keys)', async () => { + const result = await settingsProviderEngine.get('thisKeyDoesNotExist'); + await expect(result).rejects.toThrow('No setting exists for key thisKeyDoesNotExist'); +}); + +// default for key does exist + +// undefined is returned as value + +// test('Set verseRef returns true', async () => { +// const result = await settingsProviderEngine.set('platform.verseRef', NEW_VERSE_REF); +// expect(result).toBe(true); +// }); + +// test('Reset verseRef returns true', async () => { +// const result = await settingsProviderEngine.reset('platform.verseRef'); +// expect(result).toBe(true); +// }); diff --git a/src/main/services/settings.service-host.test.tsx b/src/main/services/settings.service-host.test.tsx deleted file mode 100644 index 99c30416a1..0000000000 --- a/src/main/services/settings.service-host.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { testingSettingService } from '@main/services/settings.service-host'; - -const VERSE_REF_DEFAULT = { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } }; -const NEW_VERSE_REF = { bookNum: 2, chapterNum: 2, verseNum: 2 }; -const settingsProviderEngine = testingSettingService.implementSettingDataProviderEngine(); - -jest.mock('@node/services/node-file-system.service', () => ({ - readFileText: () => { - return JSON.stringify(VERSE_REF_DEFAULT); - }, -})); - -// TODO: Update tests when edge cases are defined in functions - -test('Get verseRef returns default value', async () => { - const result = await settingsProviderEngine.get('platform.verseRef'); - expect(result).toEqual(VERSE_REF_DEFAULT.default); -}); - -test('Set verseRef returns true', async () => { - const result = await settingsProviderEngine.set('platform.verseRef', NEW_VERSE_REF); - expect(result).toBe(true); -}); - -test('Reset verseRef returns true', async () => { - const result = await settingsProviderEngine.reset('platform.verseRef'); - expect(result).toBe(true); -}); diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index 747544e401..1047c7404b 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -2,53 +2,56 @@ import IDataProviderEngine from '@shared/models/data-provider-engine.model'; import { DataProviderUpdateInstructions } from '@shared/models/data-provider.model'; import dataProviderService, { DataProviderEngine } from '@shared/services/data-provider.service'; import { + AllSettingsData, ISettingsService, SettingDataTypes, settingsServiceDataProviderName, settingsServiceObjectToProxy, } from '@shared/services/settings.service-model'; -import coreSettingsInfo, { AllSettingsInfo } from '@main/data/core-settings-info.data'; +import coreSettingsInfo from '@main/data/core-settings-info.data'; import { SettingNames, SettingTypes } from 'papi-shared-types'; -import { createSyncProxyForAsyncObject, deserialize, serialize } from 'platform-bible-utils'; +import { createSyncProxyForAsyncObject, deserialize } from 'platform-bible-utils'; import { joinUriPaths } from '@node/utils/util'; import * as nodeFS from '@node/services/node-file-system.service'; const SETTINGS_FILE_URI = joinUriPaths('data://', 'settings.json'); -let settingData = new Map(); - -async function loadSettingsFromFile() { - settingData.clear(); - const settingFileString = await nodeFS.readFileText(SETTINGS_FILE_URI); - settingData = deserialize(settingFileString); - if (typeof settingData !== 'object') +async function getSettingsDataFromFile() { + const settingsFileExists = await nodeFS.getStats(SETTINGS_FILE_URI); + if (!settingsFileExists) { + await nodeFS.writeFile(SETTINGS_FILE_URI, '{}'); + } + const settingsFileString = await nodeFS.readFileText(SETTINGS_FILE_URI); + const settingsData: AllSettingsData = deserialize(settingsFileString); + if (typeof settingsData !== 'object') throw new Error(`Settings data located in '${SETTINGS_FILE_URI}' is invalid`); + return settingsData; } class SettingDataProviderEngine extends DataProviderEngine implements IDataProviderEngine { - private settingsInfo; + private settingsData: Partial; + // eslint-disable-next-line @typescript-eslint/no-useless-constructor - constructor(settingsInfo: Partial) { + constructor(settingsData: Partial) { super(); - this.settingsInfo = settingsInfo; + this.settingsData = settingsData; + this.reset('platform.verseRef'); } // eslint-disable-next-line class-methods-use-this async get( key: SettingName, ): Promise { - const settingString = localStorage.getItem(key); - // Null is used by the external API - // eslint-disable-next-line no-null/no-null - if (settingString !== null) { - return deserialize(settingString); + if (!(key in this.settingsData)) { + return this.#getDefaultValueForKey(key); } - const settingInfo = this.settingsInfo[key]; - if (!settingInfo) throw new Error('no setting'); - return settingInfo.default; + // @ts-expect-error ts(2322) TypeScript falsely assumes that the returned value might be + // undefined. We know the value is going to be whatever the setting type is, since we just + // checked this + return this.settingsData[key]; } // eslint-disable-next-line class-methods-use-this @@ -56,17 +59,40 @@ class SettingDataProviderEngine key: SettingName, newSetting: SettingTypes[SettingName], ): Promise> { - localStorage.setItem(key, serialize(newSetting)); + try { + this.settingsData[key] = newSetting; + await nodeFS.writeFile(SETTINGS_FILE_URI, JSON.stringify(this.settingsData)); + } catch (error) { + throw new Error(`Error setting value for key '${key}': ${error}`); + } return true; } // eslint-disable-next-line class-methods-use-this async reset(key: SettingName): Promise { - localStorage.removeItem(key); - // this.notifyUpdate(''); TODO: Fix - // TODO: Add return true if successfully reset, return false otherwise + try { + delete this.settingsData[key]; + await nodeFS.writeFile(SETTINGS_FILE_URI, JSON.stringify(this.settingsData)); + } catch (error) { + return false; + } + this.notifyUpdate(''); return true; } + + // eslint-disable-next-line class-methods-use-this + #getDefaultValueForKey( + key: SettingName, + ): SettingTypes[SettingName] { + const settingInfo = coreSettingsInfo[key]; + if (!settingInfo) { + throw new Error(`No setting exists for key ${key}`); + } + if (!('default' in settingInfo)) { + throw new Error(`No default value specified for key ${key}`); + } + return settingInfo.default; + } } let initializationPromise: Promise; @@ -79,9 +105,8 @@ export async function initialize(): Promise { try { dataProvider = await dataProviderService.registerEngine( settingsServiceDataProviderName, - new SettingDataProviderEngine(coreSettingsInfo), // will be fixed when dp types are correct + new SettingDataProviderEngine(await getSettingsDataFromFile()), ); - loadSettingsFromFile(); resolve(); } catch (error) { reject(error); @@ -95,8 +120,8 @@ export async function initialize(): Promise { /** This is an internal-only export for testing purposes, and should not be used in development */ export const testingSettingService = { - implementSettingDataProviderEngine: () => { - return new SettingDataProviderEngine(coreSettingsInfo); + implementSettingDataProviderEngine: (settingsData: Partial) => { + return new SettingDataProviderEngine(settingsData); }, }; diff --git a/src/renderer/hooks/papi-hooks/use-setting.hook.ts b/src/renderer/hooks/papi-hooks/use-setting.hook.ts index 8e0ab4904a..da93cc64aa 100644 --- a/src/renderer/hooks/papi-hooks/use-setting.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-setting.hook.ts @@ -9,7 +9,8 @@ import { import { SettingDataTypes } from '@shared/services/settings.service-model'; /** - * Gets, sets and resets a setting on the papi + * Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes + * and gets updated when the setting is changed by others. * * @param key The string id that is used to identify the setting that will be stored on the papi * diff --git a/src/shared/services/settings.service-model.ts b/src/shared/services/settings.service-model.ts index b629b7209d..d1decff4dc 100644 --- a/src/shared/services/settings.service-model.ts +++ b/src/shared/services/settings.service-model.ts @@ -32,6 +32,10 @@ export type SettingDataTypes = { // '': DataProviderDataType; }; +export type AllSettingsData = { + [SettingName in SettingNames]: SettingTypes[SettingName]; +}; + declare module 'papi-shared-types' { export interface DataProviders { [settingsServiceDataProviderName]: ISettingsService; From b50eea5f05f465ae116c3f6892dd18d7a79c27b4 Mon Sep 17 00:00:00 2001 From: Jolie Rabideau Date: Mon, 12 Feb 2024 17:15:54 -0500 Subject: [PATCH 14/19] fix reset, work on unit tests --- .../services/settings.service-host.test.ts | 69 +++++++++++++------ src/main/services/settings.service-host.ts | 15 ++-- 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/main/services/settings.service-host.test.ts b/src/main/services/settings.service-host.test.ts index 6c13a864f4..676022879b 100644 --- a/src/main/services/settings.service-host.test.ts +++ b/src/main/services/settings.service-host.test.ts @@ -1,14 +1,24 @@ import { testingSettingService } from '@main/services/settings.service-host'; import { AllSettingsData } from '@shared/services/settings.service-model'; -import { SettingTypes } from 'papi-shared-types'; + +// TODO: Talk about this with TJ +// Avoids error `Argument of type '"platform.noSettingExists"' is not assignable to parameter of type 'keyof SettingTypes'.` +// It is added to SettingTypes, therefore picked up into SettingNames, but only actualized in our test code. +// By putting an item in this interface and not in PARTIAL_SETTINGS_DATA, we can declare a SettingName for a setting that doesn't exist. +declare module 'papi-shared-types' { + interface SettingTypes { + 'platform.noSettingExists': 'testType'; + 'platform.valueIsUndefined': undefined; + } +} const PARTIAL_SETTINGS_DATA: Partial = { - 'platform.interfaceLanguage': 'eng', + 'platform.interfaceLanguage': 'fre', + 'platform.valueIsUndefined': undefined, }; const VERSE_REF_DEFAULT = { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } }; -// const NEW_VERSE_REF = { bookNum: 2, chapterNum: 2, verseNum: 2 }; -const INTERFACE_LANGUAGE_DEFAULT = { default: 'eng' }; +const NEW_INTERFACE_LANGUAGE = 'spa'; const settingsProviderEngine = testingSettingService.implementSettingDataProviderEngine(PARTIAL_SETTINGS_DATA); @@ -17,35 +27,54 @@ jest.mock('@node/services/node-file-system.service', () => ({ readFileText: () => { return JSON.stringify(VERSE_REF_DEFAULT); }, + writeFile: () => { + return Promise.resolve(); + }, })); -// TODO: Update tests when edge cases are defined in functions - test('Get verseRef returns default value', async () => { const result = await settingsProviderEngine.get('platform.verseRef'); expect(result).toEqual(VERSE_REF_DEFAULT.default); }); -test('Get verseRef returns stored value', async () => { +test('Get interfaceLanguage returns stored value', async () => { const result = await settingsProviderEngine.get('platform.interfaceLanguage'); - expect(result).toEqual(INTERFACE_LANGUAGE_DEFAULT.default); + expect(result).toEqual(PARTIAL_SETTINGS_DATA['platform.interfaceLanguage']); }); -test('Key does not exist (on both settings file and list of known keys)', async () => { - const result = await settingsProviderEngine.get('thisKeyDoesNotExist'); - await expect(result).rejects.toThrow('No setting exists for key thisKeyDoesNotExist'); +test('No setting exists for key', async () => { + await expect(settingsProviderEngine.get('platform.noSettingExists')).rejects.toThrow( + 'No setting exists for key platform.noSettingExists', + ); }); -// default for key does exist +test('Undefined returned as setting value', async () => { + const result = await settingsProviderEngine.get('platform.valueIsUndefined'); + expect(result).toEqual(undefined); +}); -// undefined is returned as value +// Test `No default value specified for key ${key}` line 89 -// test('Set verseRef returns true', async () => { -// const result = await settingsProviderEngine.set('platform.verseRef', NEW_VERSE_REF); -// expect(result).toBe(true); +// Test with a key that does not exist anywhere +// ('Key does not exist (on both settings file and list of known keys)', async () => { +// const result = await settingsProviderEngine.get('thisKeyDoesNotExist'); +// await expect(result).rejects.toThrow('No setting exists for key thisKeyDoesNotExist'); // }); -// test('Reset verseRef returns true', async () => { -// const result = await settingsProviderEngine.reset('platform.verseRef'); -// expect(result).toBe(true); -// }); +test('Set verseRef returns true', async () => { + const result = await settingsProviderEngine.set( + 'platform.interfaceLanguage', + NEW_INTERFACE_LANGUAGE, + ); + expect(result).toBe(true); +}); + +test('Reset interfaceLanguage returns true', async () => { + const result = await settingsProviderEngine.reset('platform.interfaceLanguage'); + expect(result).toBe(true); +}); + +test('Reset verseRef returns false', async () => { + const result = await settingsProviderEngine.reset('platform.verseRef'); + expect(result).toBe(false); +}); diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index 1047c7404b..5cf7e3fbc6 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -34,14 +34,11 @@ class SettingDataProviderEngine { private settingsData: Partial; - // eslint-disable-next-line @typescript-eslint/no-useless-constructor constructor(settingsData: Partial) { super(); this.settingsData = settingsData; - this.reset('platform.verseRef'); } - // eslint-disable-next-line class-methods-use-this async get( key: SettingName, ): Promise { @@ -54,7 +51,6 @@ class SettingDataProviderEngine return this.settingsData[key]; } - // eslint-disable-next-line class-methods-use-this async set( key: SettingName, newSetting: SettingTypes[SettingName], @@ -68,15 +64,16 @@ class SettingDataProviderEngine return true; } - // eslint-disable-next-line class-methods-use-this async reset(key: SettingName): Promise { try { - delete this.settingsData[key]; - await nodeFS.writeFile(SETTINGS_FILE_URI, JSON.stringify(this.settingsData)); + if (this.settingsData[key]) { + delete this.settingsData[key]; + await nodeFS.writeFile(SETTINGS_FILE_URI, JSON.stringify(this.settingsData)); + } else return false; } catch (error) { - return false; + throw new Error(`Error resetting key ${key}: ${error}`); } - this.notifyUpdate(''); + this.notifyUpdate(); // TODO: How to test, what does it mean if you don't send param, need help understanding TJs comment return true; } From 91290abab13c9ff567de43e353659c6ccf11fe73 Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Tue, 13 Feb 2024 10:12:40 -0500 Subject: [PATCH 15/19] Update service host --- lib/papi-dts/papi.d.ts | 14 ++++++++----- .../services/settings.service-host.test.ts | 20 +++++++++---------- src/main/services/settings.service-host.ts | 19 +++++++++++++++--- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index 3f02a0bed3..4248484d52 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -2165,7 +2165,7 @@ declare module 'papi-shared-types' { type CommandNames = keyof CommandHandlers; interface SettingTypes { 'platform.verseRef': ScriptureReference; - placeholder: undefined; + 'platform.interfaceLanguage': string; } type SettingNames = keyof SettingTypes; /** This is just a simple example so we have more than one. It's not intended to be real. */ @@ -3894,7 +3894,7 @@ declare module 'shared/services/menu-data.service' { } declare module 'shared/services/settings.service-model' { import { SettingNames, SettingTypes } from 'papi-shared-types'; - import { OnDidDispose, Unsubscriber } from 'platform-bible-utils'; + import { OnDidDispose, UnsubscriberAsync } from 'platform-bible-utils'; import { DataProviderSubscriberOptions, DataProviderUpdateInstructions, @@ -3926,6 +3926,9 @@ declare module 'shared/services/settings.service-model' { * instead. However, do note that the unnamed data type (`''`) is fully functional. */ export type SettingDataTypes = {}; + export type AllSettingsData = { + [SettingName in SettingNames]: SettingTypes[SettingName]; + }; module 'papi-shared-types' { interface DataProviders { [settingsServiceDataProviderName]: ISettingsService; @@ -3974,8 +3977,8 @@ declare module 'shared/services/settings.service-model' { subscribe( key: SettingName, callback: (newSetting: SettingTypes[SettingName]) => void, - options: DataProviderSubscriberOptions, - ): Promise; + options?: DataProviderSubscriberOptions, + ): Promise; } & OnDidDispose & IDataProvider & typeof settingsServiceObjectToProxy; @@ -4457,7 +4460,8 @@ declare module 'renderer/hooks/papi-hooks/use-setting.hook' { } from 'shared/models/data-provider.model'; import { SettingDataTypes } from 'shared/services/settings.service-model'; /** - * Gets, sets and resets a setting on the papi + * Gets, sets and resets a setting on the papi. Also notifies subscribers when the setting changes + * and gets updated when the setting is changed by others. * * @param key The string id that is used to identify the setting that will be stored on the papi * diff --git a/src/main/services/settings.service-host.test.ts b/src/main/services/settings.service-host.test.ts index 676022879b..80af2f69c9 100644 --- a/src/main/services/settings.service-host.test.ts +++ b/src/main/services/settings.service-host.test.ts @@ -7,14 +7,14 @@ import { AllSettingsData } from '@shared/services/settings.service-model'; // By putting an item in this interface and not in PARTIAL_SETTINGS_DATA, we can declare a SettingName for a setting that doesn't exist. declare module 'papi-shared-types' { interface SettingTypes { - 'platform.noSettingExists': 'testType'; - 'platform.valueIsUndefined': undefined; + 'testingOnly.noSettingExists': 'testType'; + 'testingOnly.valueIsUndefined': undefined; } } const PARTIAL_SETTINGS_DATA: Partial = { 'platform.interfaceLanguage': 'fre', - 'platform.valueIsUndefined': undefined, + 'testingOnly.valueIsUndefined': undefined, }; const VERSE_REF_DEFAULT = { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } }; @@ -43,23 +43,23 @@ test('Get interfaceLanguage returns stored value', async () => { }); test('No setting exists for key', async () => { - await expect(settingsProviderEngine.get('platform.noSettingExists')).rejects.toThrow( + await expect(settingsProviderEngine.get('testingOnly.noSettingExists')).rejects.toThrow( 'No setting exists for key platform.noSettingExists', ); }); test('Undefined returned as setting value', async () => { - const result = await settingsProviderEngine.get('platform.valueIsUndefined'); + const result = await settingsProviderEngine.get('testingOnly.valueIsUndefined'); expect(result).toEqual(undefined); }); // Test `No default value specified for key ${key}` line 89 -// Test with a key that does not exist anywhere -// ('Key does not exist (on both settings file and list of known keys)', async () => { -// const result = await settingsProviderEngine.get('thisKeyDoesNotExist'); -// await expect(result).rejects.toThrow('No setting exists for key thisKeyDoesNotExist'); -// }); +test('Key does not exist (on both settings file and list of known keys)', async () => { + // @ts-expect-error ts(2345) + const result = settingsProviderEngine.get('thisKeyDoesNotExist'); + await expect(result).rejects.toThrow('No setting exists for key thisKeyDoesNotExist'); +}); test('Set verseRef returns true', async () => { const result = await settingsProviderEngine.set( diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index 5cf7e3fbc6..f5ad30fd77 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -1,5 +1,8 @@ import IDataProviderEngine from '@shared/models/data-provider-engine.model'; -import { DataProviderUpdateInstructions } from '@shared/models/data-provider.model'; +import { + DataProviderDataType, + DataProviderUpdateInstructions, +} from '@shared/models/data-provider.model'; import dataProviderService, { DataProviderEngine } from '@shared/services/data-provider.service'; import { AllSettingsData, @@ -29,7 +32,17 @@ async function getSettingsDataFromFile() { } class SettingDataProviderEngine - extends DataProviderEngine + extends DataProviderEngine< + SettingDataTypes & { + // Including `''` here so we can emit `''` events though the event types are not + // tight enough to use on the actual `''` data type and methods + '': DataProviderDataType< + SettingNames, + SettingTypes[SettingNames], + SettingTypes[SettingNames] + >; + } + > implements IDataProviderEngine { private settingsData: Partial; @@ -73,7 +86,7 @@ class SettingDataProviderEngine } catch (error) { throw new Error(`Error resetting key ${key}: ${error}`); } - this.notifyUpdate(); // TODO: How to test, what does it mean if you don't send param, need help understanding TJs comment + this.notifyUpdate(''); return true; } From 4baf9a1443134e698408deac896688f2dd61bd54 Mon Sep 17 00:00:00 2001 From: Jolie Rabideau Date: Tue, 13 Feb 2024 10:41:37 -0500 Subject: [PATCH 16/19] finish unit tests --- .../services/settings.service-host.test.ts | 49 ++++++++++--------- src/main/services/settings.service-host.ts | 7 +-- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/main/services/settings.service-host.test.ts b/src/main/services/settings.service-host.test.ts index 80af2f69c9..0ab7bb56d5 100644 --- a/src/main/services/settings.service-host.test.ts +++ b/src/main/services/settings.service-host.test.ts @@ -1,27 +1,15 @@ import { testingSettingService } from '@main/services/settings.service-host'; -import { AllSettingsData } from '@shared/services/settings.service-model'; -// TODO: Talk about this with TJ -// Avoids error `Argument of type '"platform.noSettingExists"' is not assignable to parameter of type 'keyof SettingTypes'.` -// It is added to SettingTypes, therefore picked up into SettingNames, but only actualized in our test code. -// By putting an item in this interface and not in PARTIAL_SETTINGS_DATA, we can declare a SettingName for a setting that doesn't exist. -declare module 'papi-shared-types' { - interface SettingTypes { - 'testingOnly.noSettingExists': 'testType'; - 'testingOnly.valueIsUndefined': undefined; - } -} - -const PARTIAL_SETTINGS_DATA: Partial = { +const MOCK_SETTINGS_DATA = { 'platform.interfaceLanguage': 'fre', - 'testingOnly.valueIsUndefined': undefined, + 'settingsTest.valueIsUndefined': undefined, }; const VERSE_REF_DEFAULT = { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } }; const NEW_INTERFACE_LANGUAGE = 'spa'; const settingsProviderEngine = - testingSettingService.implementSettingDataProviderEngine(PARTIAL_SETTINGS_DATA); + testingSettingService.implementSettingDataProviderEngine(MOCK_SETTINGS_DATA); jest.mock('@node/services/node-file-system.service', () => ({ readFileText: () => { @@ -31,6 +19,15 @@ jest.mock('@node/services/node-file-system.service', () => ({ return Promise.resolve(); }, })); +jest.mock('@main/data/core-settings-info.data', () => ({ + ...jest.requireActual('@main/data/core-settings-info.data'), + __esModule: true, + default: { + 'platform.verseRef': { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } }, + 'platform.interfaceLanguage': { default: 'eng' }, + 'settingsTest.noDefaultExists': {}, + }, +})); test('Get verseRef returns default value', async () => { const result = await settingsProviderEngine.get('platform.verseRef'); @@ -39,26 +36,30 @@ test('Get verseRef returns default value', async () => { test('Get interfaceLanguage returns stored value', async () => { const result = await settingsProviderEngine.get('platform.interfaceLanguage'); - expect(result).toEqual(PARTIAL_SETTINGS_DATA['platform.interfaceLanguage']); + expect(result).toEqual(MOCK_SETTINGS_DATA['platform.interfaceLanguage']); }); test('No setting exists for key', async () => { - await expect(settingsProviderEngine.get('testingOnly.noSettingExists')).rejects.toThrow( - 'No setting exists for key platform.noSettingExists', + // settingsTest.noSettingExists does not exist on SettingNames + // @ts-expect-error ts(2345) + await expect(settingsProviderEngine.get('settingsTest.noSettingExists')).rejects.toThrow( + 'No setting exists for key settingsTest.noSettingExists', ); }); test('Undefined returned as setting value', async () => { - const result = await settingsProviderEngine.get('testingOnly.valueIsUndefined'); + // settingsTest.valueIsUndefined does not exist on SettingNames + // @ts-expect-error ts(2345) + const result = await settingsProviderEngine.get('settingsTest.valueIsUndefined'); expect(result).toEqual(undefined); }); -// Test `No default value specified for key ${key}` line 89 - -test('Key does not exist (on both settings file and list of known keys)', async () => { +test('No default specified for key', async () => { + // settingsTest.noDefaultExists does not exist on SettingNames // @ts-expect-error ts(2345) - const result = settingsProviderEngine.get('thisKeyDoesNotExist'); - await expect(result).rejects.toThrow('No setting exists for key thisKeyDoesNotExist'); + await expect(settingsProviderEngine.get('settingsTest.noDefaultExists')).rejects.toThrow( + 'No default value specified for key settingsTest.noDefaultExists', + ); }); test('Set verseRef returns true', async () => { diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index f5ad30fd77..f37b907027 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -58,9 +58,9 @@ class SettingDataProviderEngine if (!(key in this.settingsData)) { return this.#getDefaultValueForKey(key); } - // @ts-expect-error ts(2322) TypeScript falsely assumes that the returned value might be - // undefined. We know the value is going to be whatever the setting type is, since we just - // checked this + // TypeScript falsely assumes that the returned value might be undefined. We know + // the value is going to be whatever the setting type is, since we just checked this + // @ts-expect-error ts(2322) return this.settingsData[key]; } @@ -90,6 +90,7 @@ class SettingDataProviderEngine return true; } + // Internal function used by get, does not need to use this // eslint-disable-next-line class-methods-use-this #getDefaultValueForKey( key: SettingName, From dcda49620bac8c69aab85f9ba36571c345350d24 Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Tue, 13 Feb 2024 15:40:51 -0500 Subject: [PATCH 17/19] Processed some more review comments --- lib/papi-dts/papi.d.ts | 2 +- src/declarations/papi-shared-types.ts | 2 +- src/main/data/core-settings-info.data.ts | 2 +- .../services/settings.service-host.test.ts | 6 +-- src/main/services/settings.service-host.ts | 49 ++++++++++--------- 5 files changed, 32 insertions(+), 29 deletions(-) diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index 4248484d52..669e3ddb08 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -2165,7 +2165,7 @@ declare module 'papi-shared-types' { type CommandNames = keyof CommandHandlers; interface SettingTypes { 'platform.verseRef': ScriptureReference; - 'platform.interfaceLanguage': string; + 'platform.interfaceLanguage': string[]; } type SettingNames = keyof SettingTypes; /** This is just a simple example so we have more than one. It's not intended to be real. */ diff --git a/src/declarations/papi-shared-types.ts b/src/declarations/papi-shared-types.ts index bc8555c036..f1938bc1f7 100644 --- a/src/declarations/papi-shared-types.ts +++ b/src/declarations/papi-shared-types.ts @@ -55,7 +55,7 @@ declare module 'papi-shared-types' { export interface SettingTypes { 'platform.verseRef': ScriptureReference; - 'platform.interfaceLanguage': string; + 'platform.interfaceLanguage': string[]; } export type SettingNames = keyof SettingTypes; diff --git a/src/main/data/core-settings-info.data.ts b/src/main/data/core-settings-info.data.ts index fcd96a5f27..bcea7f827a 100644 --- a/src/main/data/core-settings-info.data.ts +++ b/src/main/data/core-settings-info.data.ts @@ -13,7 +13,7 @@ export type AllSettingsInfo = { /** Info about all settings built into core. Does not contain info for extensions' settings */ const coreSettingsInfo: Partial = { 'platform.verseRef': { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } }, - 'platform.interfaceLanguage': { default: 'eng' }, + 'platform.interfaceLanguage': { default: ['eng'] }, }; export default coreSettingsInfo; diff --git a/src/main/services/settings.service-host.test.ts b/src/main/services/settings.service-host.test.ts index 0ab7bb56d5..d7c93c0da8 100644 --- a/src/main/services/settings.service-host.test.ts +++ b/src/main/services/settings.service-host.test.ts @@ -1,12 +1,12 @@ import { testingSettingService } from '@main/services/settings.service-host'; const MOCK_SETTINGS_DATA = { - 'platform.interfaceLanguage': 'fre', + 'platform.interfaceLanguage': ['fre'], 'settingsTest.valueIsUndefined': undefined, }; const VERSE_REF_DEFAULT = { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } }; -const NEW_INTERFACE_LANGUAGE = 'spa'; +const NEW_INTERFACE_LANGUAGE = ['spa']; const settingsProviderEngine = testingSettingService.implementSettingDataProviderEngine(MOCK_SETTINGS_DATA); @@ -24,7 +24,7 @@ jest.mock('@main/data/core-settings-info.data', () => ({ __esModule: true, default: { 'platform.verseRef': { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } }, - 'platform.interfaceLanguage': { default: 'eng' }, + 'platform.interfaceLanguage': { default: ['eng'] }, 'settingsTest.noDefaultExists': {}, }, })); diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index f37b907027..1b0bfe4762 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -31,6 +31,23 @@ async function getSettingsDataFromFile() { return settingsData; } +async function writeSettingsDataToFile(settingsData: Partial) { + await nodeFS.writeFile(SETTINGS_FILE_URI, JSON.stringify(settingsData)); +} + +function getDefaultValueForKey( + key: SettingName, +): SettingTypes[SettingName] { + const settingInfo = coreSettingsInfo[key]; + if (!settingInfo) { + throw new Error(`No setting exists for key ${key}`); + } + if (!('default' in settingInfo)) { + throw new Error(`No default value specified for key ${key}`); + } + return settingInfo.default; +} + class SettingDataProviderEngine extends DataProviderEngine< SettingDataTypes & { @@ -56,7 +73,7 @@ class SettingDataProviderEngine key: SettingName, ): Promise { if (!(key in this.settingsData)) { - return this.#getDefaultValueForKey(key); + return getDefaultValueForKey(key); } // TypeScript falsely assumes that the returned value might be undefined. We know // the value is going to be whatever the setting type is, since we just checked this @@ -70,7 +87,7 @@ class SettingDataProviderEngine ): Promise> { try { this.settingsData[key] = newSetting; - await nodeFS.writeFile(SETTINGS_FILE_URI, JSON.stringify(this.settingsData)); + writeSettingsDataToFile(this.settingsData); } catch (error) { throw new Error(`Error setting value for key '${key}': ${error}`); } @@ -79,30 +96,16 @@ class SettingDataProviderEngine async reset(key: SettingName): Promise { try { - if (this.settingsData[key]) { - delete this.settingsData[key]; - await nodeFS.writeFile(SETTINGS_FILE_URI, JSON.stringify(this.settingsData)); - } else return false; + if (!this.settingsData[key]) { + return false; + } + delete this.settingsData[key]; + writeSettingsDataToFile(this.settingsData); + this.notifyUpdate(''); + return true; } catch (error) { throw new Error(`Error resetting key ${key}: ${error}`); } - this.notifyUpdate(''); - return true; - } - - // Internal function used by get, does not need to use this - // eslint-disable-next-line class-methods-use-this - #getDefaultValueForKey( - key: SettingName, - ): SettingTypes[SettingName] { - const settingInfo = coreSettingsInfo[key]; - if (!settingInfo) { - throw new Error(`No setting exists for key ${key}`); - } - if (!('default' in settingInfo)) { - throw new Error(`No default value specified for key ${key}`); - } - return settingInfo.default; } } From 7c0d51d73309475578210bd3d1c2c01709aab2f9 Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Tue, 13 Feb 2024 16:56:03 -0500 Subject: [PATCH 18/19] Add beforeEach function for tests, and deep clone data passed into data provider engine constructor --- src/main/services/settings.service-host.test.ts | 10 ++++++++-- src/main/services/settings.service-host.ts | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/services/settings.service-host.test.ts b/src/main/services/settings.service-host.test.ts index d7c93c0da8..5d315f33e8 100644 --- a/src/main/services/settings.service-host.test.ts +++ b/src/main/services/settings.service-host.test.ts @@ -8,8 +8,14 @@ const MOCK_SETTINGS_DATA = { const VERSE_REF_DEFAULT = { default: { bookNum: 1, chapterNum: 1, verseNum: 1 } }; const NEW_INTERFACE_LANGUAGE = ['spa']; -const settingsProviderEngine = - testingSettingService.implementSettingDataProviderEngine(MOCK_SETTINGS_DATA); +let settingsProviderEngine: ReturnType< + typeof testingSettingService.implementSettingDataProviderEngine +>; + +beforeEach(() => { + settingsProviderEngine = + testingSettingService.implementSettingDataProviderEngine(MOCK_SETTINGS_DATA); +}); jest.mock('@node/services/node-file-system.service', () => ({ readFileText: () => { diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index 1b0bfe4762..d24e183905 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -13,7 +13,7 @@ import { } from '@shared/services/settings.service-model'; import coreSettingsInfo from '@main/data/core-settings-info.data'; import { SettingNames, SettingTypes } from 'papi-shared-types'; -import { createSyncProxyForAsyncObject, deserialize } from 'platform-bible-utils'; +import { createSyncProxyForAsyncObject, deserialize, serialize } from 'platform-bible-utils'; import { joinUriPaths } from '@node/utils/util'; import * as nodeFS from '@node/services/node-file-system.service'; @@ -66,7 +66,7 @@ class SettingDataProviderEngine constructor(settingsData: Partial) { super(); - this.settingsData = settingsData; + this.settingsData = deserialize(serialize(settingsData)); } async get( From 1489050b42d49c99172b79003e39dca59e90c24e Mon Sep 17 00:00:00 2001 From: Rolf Heij Date: Wed, 14 Feb 2024 08:36:06 -0500 Subject: [PATCH 19/19] Small update to settings service host --- src/main/services/settings.service-host.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/services/settings.service-host.ts b/src/main/services/settings.service-host.ts index d24e183905..b4ea64c7b3 100644 --- a/src/main/services/settings.service-host.ts +++ b/src/main/services/settings.service-host.ts @@ -87,7 +87,7 @@ class SettingDataProviderEngine ): Promise> { try { this.settingsData[key] = newSetting; - writeSettingsDataToFile(this.settingsData); + await writeSettingsDataToFile(this.settingsData); } catch (error) { throw new Error(`Error setting value for key '${key}': ${error}`); } @@ -96,11 +96,11 @@ class SettingDataProviderEngine async reset(key: SettingName): Promise { try { - if (!this.settingsData[key]) { + if (!(key in this.settingsData)) { return false; } delete this.settingsData[key]; - writeSettingsDataToFile(this.settingsData); + await writeSettingsDataToFile(this.settingsData); this.notifyUpdate(''); return true; } catch (error) {