From 50242b3893f85f87130112ec6b2e999212dd1185 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 28 Apr 2023 13:05:42 -0500 Subject: [PATCH 1/4] Tracked papi.d.ts since it is an important interface to keep consistent --- lib/papi-dts/.gitignore | 1 - lib/papi-dts/papi.d.ts | 2122 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 2122 insertions(+), 1 deletion(-) create mode 100644 lib/papi-dts/papi.d.ts diff --git a/lib/papi-dts/.gitignore b/lib/papi-dts/.gitignore index 76370ed8d1..f747549e1e 100644 --- a/lib/papi-dts/.gitignore +++ b/lib/papi-dts/.gitignore @@ -1,2 +1 @@ -papi.d.ts papi.tsbuildinfo diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts new file mode 100644 index 0000000000..a52f0b2f4c --- /dev/null +++ b/lib/papi-dts/papi.d.ts @@ -0,0 +1,2122 @@ +/// +declare module 'shared/utils/papi-util' { + /** Function to run to dispose of something. Returns true if successfully unsubscribed */ + export type Unsubscriber = () => boolean; + /** Object containing both a function to run to dispose of something and a promise that resolves when that thing is done subscribing */ + export type UnsubPromise = { + /** Promise that resolves when done registering */ + promise: Promise; + /** Unsubscriber function that unregisters */ + unsubscriber: Unsubscriber; + }; + /** + * Returns an Unsubscriber function that combines all the unsubscribers passed in. + * @param unsubscribers all unsubscribers to aggregate into one unsubscriber + * @returns function that unsubscribes from all passed in unsubscribers when run + */ + export const aggregateUnsubscribers: (unsubscribers: Unsubscriber[]) => Unsubscriber; + /** Function to run to dispose of something that runs asynchronously. The promise resolves to true if successfully unsubscribed */ + export type UnsubscriberAsync = () => Promise; + /** Object containing both a function to run to dispose of something and a promise that resolves when that thing is done subscribing */ + export type UnsubPromiseAsync = { + /** Promise that resolves when done registering */ + promise: Promise; + /** Unsubscriber function that unregisters */ + unsubscriber: UnsubscriberAsync; + }; + /** + * Returns an UnsubscriberAsync function that combines all the unsubscribers passed in. + * @param unsubscribers all unsubscribers to aggregate into one unsubscriber + * @returns function that unsubscribes from all passed in unsubscribers when run + */ + export const aggregateUnsubscriberAsyncs: ( + unsubscribers: UnsubscriberAsync[], + ) => UnsubscriberAsync; + /** + * Creates a safe version of a register function that returns an UnsubPromiseAsync. + * This is a challenge because we want to provide an unsubscriber that functions + * even before the UnsubPromise.promise resolves. + * TODO: This isn't quite fully safe yet. See TODO below. Basically, if you run this + * before initializing, the unsubscriber returned may not work if you call it + * immediately, but it will also throw an exception (we can remove this if we + * actually run into this case and it seems to work fine). You should wait to call the unsubscriber later + * @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 unsubPromiseAsync function is initialized + * @param initialize promise that resolves when the service is finished initializing + * @param backupUnregisterFn a backup unsubscriber function that should attempt to unsubscribe whatever the unsafeRegisterFn is subscribing before unsafeRegisterFn finishes subscribing and resolves. Will be overwritten with the actual unsubscriber once the unsafeRegisterFn promise resolves. See TODO above for more info + * @returns safe version of an unsafe function that returns an UnsubPromiseAsync (meaning it will wait to register until the service is initialized) + */ + export const createSafeRegisterFn: ( + unsafeRegisterFn: (...args: TParam) => UnsubPromiseAsync, + isInitialized: boolean, + initialize: () => Promise, + backupUnregisterFn?: ((...args: TParam) => Promise) | undefined, + ) => (...args: TParam) => UnsubPromiseAsync; + /** + * 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 = { + 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', + } + /** + * Handler function for a command. Called when a command is executed. + * The function should accept the command's parameters as its parameters. + * The function should return a promise that resolves with the "return" value of the command. + */ + export type CommandHandler = any[], TReturn = any> = ( + ...args: TParam + ) => Promise | TReturn; + /** 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; + }; + /** + * 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 const serializeRequestType: (category: string, directive: string) => string; + /** Split a request message requestType string into its parts */ + export const deserializeRequestType: (requestType: string) => RequestType; + /** Check that two objects are deeply equal, comparing members of each object and such */ + export function deepEqual(a: unknown, b: unknown): boolean; + /** + * HTML Encodes the provided string. + * Thanks to ChatGPT + * @param str string to HTML encode + * @returns HTML-encoded string + */ + export const htmlEncode: (str: string) => string; +} +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 } from 'shared/utils/papi-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; + }>; + /** 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 = { + senderId: number; + 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: string, + 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/utils/util' { + export function newGuid(): string; + /** + * 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; + /** + * Determine whether the object is a string + * @param o object to determine if it is a string + * @returns true if the object is a string; false otherwise + */ + export function isString(o: unknown): o is string; + /** + * Get a function that reduces calls to the function passed in + * @param fn The function to debounce + * @param delay How much delay in milliseconds after the most recent call to the debounced function to call the function + * @returns function that, when called, only calls the function passed in at maximum every delay ms + */ + export function debounce void>(fn: T, delay?: number): T; + /** + * Groups each item in the array of items into a map according to the keySelector + * @param items array of items to group by + * @param keySelector function to run on each item to get the key for the group to which it belongs + * @param valueSelector function to run on each item to get the value it should have in the group (like map function). If not provided, uses the item itself + * @returns map of keys to groups of values corresponding to each item + */ + export function groupBy(items: T[], keySelector: (item: T) => K): Map>; + export function groupBy( + items: T[], + keySelector: (item: T) => K, + valueSelector: (item: T) => V, + ): Map>; + /** + * Function to get an error message from the object (useful for getting error message in a catch block) + * @param error error object whose message to get + * @returns message of the error - if object has message, returns message. Otherwise tries to stringify + * @example + * try {...} + * catch (e) { logger.info(getErrorMessage(e)) } + */ + export function getErrorMessage(error: unknown): string; + /** + * Asynchronously waits for the specified number of milliseconds. + * (wraps setTimeout in a promise) + */ + export function wait(ms: number): Promise; + /** + * Runs the specified function and will timeout if it takes longer than the specified wait time + * @param fn The function to run + * @param maxWaitTimeInMS The maximum amount of time to wait for the function to resolve + * @returns Promise that resolves to the resolved value of the function or null if it + * ran longer than the specified wait time + */ + export function waitForDuration( + fn: () => Promise, + maxWaitTimeInMS: number, + ): Promise | null>; + /** + * Generic container so we don't need to have XYZContainer types whenever we need to wrap something. + * This type is basically a pointer to an object. + */ + export interface Container { + contents: T | undefined; + } +} +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/global-this.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; + } + /** Type of Paranext process */ + export enum ProcessType { + Main = 'main', + Renderer = 'renderer', + ExtensionHost = 'extension-host', + } +} +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; + /** + * 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 | null; + }; + /** 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'; + /** + * 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; + const logger: log.Logger & { + default: log.Logger; + }; + export default logger; +} +declare module 'shared/models/papi-event.model' { + import { Unsubscriber, UnsubscriberAsync } from 'shared/utils/papi-util'; + /** Callback function that accepts an event and should run when an event is emitted */ + export type PapiEventHandler = (event: T) => void; + /** + * Function that subscribes the provided callback to run when this event is emitted. + * @param callback function to run with the event when it is emitted + * @returns unsubscriber function to run to stop calling the passed-in function when the event is emitted + */ + export type PapiEvent = (callback: PapiEventHandler) => Unsubscriber; + /** + * A PapiEvent that subscribes asynchronously and resolves an asynchronous unsubscriber. + * + * Note: The callback itself is not asynchronous. + */ + export type PapiEventAsync = (callback: PapiEventHandler) => Promise; +} +declare module 'shared/models/papi-event-emitter.model' { + /** + * Interfaces, classes, and functions related to events and event emitters + */ + import { PapiEvent } from 'shared/models/papi-event.model'; + /** + * Event manager - accepts subscriptions to an event and runs the subscription callbacks when the event is emitted + * 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. + */ + export default class PapiEventEmitter { + /** + * Subscribes a function to run when this event is emitted. + * @alias event + * @param callback function to run with the event when it is emitted + * @returns unsubscriber function to run to stop calling the passed-in function when the event is emitted + */ + subscribe: PapiEvent; + /** All callback functions that will run when this event is emitted. Lazy loaded */ + private subscriptions?; + /** Event for listeners to subscribe to. Lazy loaded */ + private lazyEvent?; + /** Whether this emitter has been disposed */ + private isDisposed; + /** + * Event for listeners to subscribe to. Subscribes a function to run when this event is emitted. + * Use like `const unsubscriber = event(callback)` + * @param callback function to run with the event when it is emitted + * @returns unsubscriber function to run to stop calling the passed-in function when the event is emitted + */ + get event(): PapiEvent; + /** Disposes of this event, preparing it to release from memory */ + dispose: () => void; + /** + * Runs the subscriptions for the event + * @param event event data to provide to subscribed callbacks + */ + emit: (event: T) => void; + /** + * Function that runs the subscriptions for the event. + * Added here so children can override emit and still call the base functionality. + * See NetworkEventEmitter.emit for example + */ + protected emitFn(event: T): void; + /** Check to make sure this emitter is not disposed. Throw if it is */ + protected assertNotDisposed(): void; + /** + * Disposes of this event, preparing it to release from memory. + * Added here so children can override emit and still call the base functionality. + */ + protected disposeFn(): void; + } +} +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.model' { + /** + * The renderer's implementation of WebSocket is the browser-supplied WebSocket, which doesn't work in Node + */ + export default WebSocket; +} +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; + /** + * 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< + 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; + /** + * 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'; + /** + * 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/papi-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 { PapiEventHandler } from 'shared/models/papi-event.model'; + import PapiEventEmitter from 'shared/models/papi-event-emitter.model'; + /** + * 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 PapiEventEmitter { + /** 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: PapiEventHandler, + /** 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: () => void; + } +} +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 + */ + import { ClientConnectEvent, ClientDisconnectEvent } from 'shared/data/internal-connection.model'; + import { + CommandHandler, + ComplexRequest, + ComplexResponse, + RequestHandlerType, + UnsubPromiseAsync, + } from 'shared/utils/papi-util'; + import PapiEventEmitter from 'shared/models/papi-event-emitter.model'; + import { PapiEvent } from 'shared/models/papi-event.model'; + /** + * 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> = CommandHandler< + TParam, + 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: PapiEvent; + /** Event that emits with clientId when a client disconnects */ + export const onDidClientDisconnect: PapiEvent; + /** 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: string, + ...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: string, + handler: ArgsRequestHandler, + handlerType?: RequestHandlerType, + ): UnsubPromiseAsync; + export function registerRequestHandler( + requestType: string, + handler: ContentsRequestHandler, + handlerType?: RequestHandlerType, + ): UnsubPromiseAsync; + export function registerRequestHandler( + requestType: string, + handler: ComplexRequestHandler, + handlerType?: RequestHandlerType, + ): UnsubPromiseAsync; + /** + * 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) => PapiEventEmitter; + /** + * 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) => PapiEvent; + /** + * 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: string, + ) => (...args: TParam) => Promise; + /** All the exports in this service that are to be exposed on the PAPI */ + export const papiNetworkService: { + onDidClientConnect: PapiEvent; + onDidClientDisconnect: PapiEvent; + createNetworkEventEmitter: (eventType: string) => PapiEventEmitter; + getNetworkEvent: (eventType: string) => PapiEvent; + }; +} +declare module 'shared/services/command.service' { + import { CommandHandler, UnsubPromiseAsync } from 'shared/utils/papi-util'; + /** + * Register a command on the papi to be handled here. + * + * WARNING: THIS DOES NOT CHECK FOR INITIALIZATION. DO NOT USE OUTSIDE OF INITIALIZATION. Use registerCommand + * @param commandName command name to register for handling here + * @param handler function to run when the command is invoked + * @returns promise that resolves if the request successfully registered and unsubscriber function to run to stop the passed-in function from handling requests + */ + export const registerCommandUnsafe: ( + commandName: string, + handler: CommandHandler, + ) => UnsubPromiseAsync; + /** 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: string, + ...args: TParam + ) => 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: string, + ) => (...args: TParam) => Promise; + /** + * Register a command on the papi to be handled here + * @param commandName command name to register for handling here + * @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: string, + handler: CommandHandler, + ) => UnsubPromiseAsync; +} +declare module 'shared/data/web-view.model' { + import { ReactNode } from 'react'; + /** + * Information used to recreate a tab + */ + export type SavedTabInfo = { + /** + * The underlying tab type. Used to determine which extension owns it. + */ + type: string; + /** + * Data needed to recreate the tab during load + */ + data?: unknown; + }; + /** + * Information needed to create a tab inside of Paranext + */ + export type TabInfo = { + /** + * The underlying tab type. Used to determine which extension owns it. + */ + type: string; + /** + * Text to show on the title bar of the tab + */ + title: string; + /** + * Content to show inside the tab + */ + content: ReactNode; + /** + * (optional) Minimum width that the tab can become + */ + minWidth?: number; + /** + * (optional) Minimum height that the tab can become + */ + minHeight?: number; + }; + /** + * For now all tab creators must do their own data type verification + */ + export type TabCreator = (tabData: SavedTabInfo) => TabInfo; + export enum WebViewContentType { + React = 'react', + HTML = 'html', + } + /** Base WebView properties that all WebViews share */ + type WebViewContentsBase = { + contents: string; + title?: string; + }; + /** WebView representation using React */ + export type WebViewContentsReact = WebViewContentsBase & { + contentType?: WebViewContentType.React; + componentName: string; + styles?: string; + }; + /** WebView representation using HTML */ + export type WebViewContentsHtml = WebViewContentsBase & { + contentType: WebViewContentType.HTML; + }; + /** WebView definition created by extensions to show web content */ + export type WebViewContents = WebViewContentsReact | WebViewContentsHtml; +} +declare module 'renderer/components/web-view.component' { + import { WebViewContents } from 'shared/data/web-view.model'; + export type WebViewProps = Omit; + export function WebView({ contents, title, contentType }: WebViewProps): JSX.Element; +} +declare module 'shared/services/web-view.service' { + /** + * Service that handles WebView-related operations + */ + import { WebViewProps } from 'renderer/components/web-view.component'; + import { WebViewContents } from 'shared/data/web-view.model'; + /** Event emitted when webViews are added */ + export type AddWebViewEvent = { + webView: WebViewProps; + }; + /** Event that emits with webView info when a webView is added */ + export const onDidAddWebView: import('shared/models/papi-event.model').PapiEvent; + /** + * Adds a WebView and runs all event handlers who are listening to this event + * @param webView full html document to set as the webview iframe contents. Can be shortened to just a string + * @returns promise that resolves nothing if we successfully handled the webView + */ + export const addWebView: (webView: WebViewContents) => Promise; + /** Sets up the WebViewService. Runs only once */ + export const initialize: () => Promise; +} +declare module 'shared/services/internet.service' { + const internetService: { + fetch: typeof fetch; + }; + export default internetService; +} +declare module 'shared/models/data-provider.interface' { + import { UnsubscriberAsync } from 'shared/utils/papi-util'; + import { PapiEventHandler } from 'shared/models/papi-event.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. + * - `'all'` - 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' | 'all'; + }; + /** + * Subscribe to receive updates from this data provider that are relevant to the provided selector. + * + * 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: TSelector, + callback: PapiEventHandler, + options?: DataProviderSubscriberOptions, + ) => Promise; + /** + * 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. + * @type `TSelector` - the type of selector used to get some data from this provider. + * A selector is an object a caller provides to the data provider to tell the provider what subset of data it wants. + * Note: A selector must be stringifiable. + * @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 + */ + interface IDataProvider { + /** + * 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 true if successfully set (will send updates), false otherwise (will not send updates) + */ + set: (selector: TSelector, data: TSetData) => 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 + */ + get: (selector: TSelector) => Promise; + /** + * Subscribe to receive updates from this data provider that are relevant to the provided selector. + * + * 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 + */ + subscribe: DataProviderSubscriber; + } + export default IDataProvider; +} +declare module 'shared/models/disposal.model' { + import { PapiEvent } from 'shared/models/papi-event.model'; + import { UnsubscriberAsync } from 'shared/utils/papi-util'; + /** Require a `dispose` function */ + export interface Dispose { + /** Release resources and notify dependent services when tearing down an object */ + dispose: UnsubscriberAsync; + } + /** Require an `onDidDispose` event */ + export interface OnDidDispose { + /** Event that emits when `dispose` is called on an object */ + onDidDispose: PapiEvent; + } + /** Indicates than an object cannot have an `onDidDispose` event. + * Also allows an object to include a `dispose` function. */ + export type CannotHaveOnDidDispose = T & { + /** Release resources and notify dependent services when tearing down an object */ + dispose?: UnsubscriberAsync; + /** Event that emits when `dispose` is called on an object */ + onDidDispose?: undefined; + }; +} +declare module 'shared/models/data-provider.model' { + import IDataProvider from 'shared/models/data-provider.interface'; + import { Dispose, OnDidDispose } from 'shared/models/disposal.model'; + /** + * Information about a data provider. + * Returned from getting a data provider. + */ + export type DataProvider = OnDidDispose & + IDataProvider; + /** + * Information about a data provider including control over disposing of it. + * Returned from registering a data provider (only the process that set it up should dispose of it) + */ + export type DisposableDataProvider = Dispose & + DataProvider; +} +declare module 'shared/models/data-provider-engine.model' { + /** + * The object to register with the DataProviderService to create a data provider. + * The DataProviderService creates a IDataProvider on the papi that layers over this engine, providing special functionality + * + * Note: methods on objects that implement this interface must be unbound functions, not arrow functions. + * @type `TSelector` - the type of selector used to get some data from this provider. + * A selector is an object a caller provides to the data provider to tell the provider what subset of data it wants. + * @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 + */ + interface IDataProviderEngine { + /** + * Method to run to send clients updates outside of the `set` method. + * papi overwrites this function on the DataProviderEngine itself to emit an update after running the defined `notifyUpdate` method in the DataProviderEngine. + * + * WARNING: Never run this in the `get` method! It will create a destructive infinite loop. + * + * @returns true if we should send updates, false otherwise (will not send updates). Same return as `set` + */ + notifyUpdate?(): boolean; + /** + * Set a subset of data according to the selector. + * papi overwrites this function on the DataProviderEngine itself to emit an update after running the defined `set` method in the DataProviderEngine. + * + * Note: you could consider this data provider to be read-only if this method is not provided. + * + * WARNING: Do not run this recursively in its own `set` method! It will create as many updates as you run `set` methods. + * @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 true if successfully set (will send updates), false otherwise (will not send updates) + */ + set?(selector: TSelector, data: TSetData): Promise; + /** + * Get a subset of data from the provider according to the selector. + * Run by the data provider on get + * @param selector tells the provider what subset of data to get + * @returns the subset of data represented by the selector + */ + get(selector: TSelector): Promise; + } + export default IDataProviderEngine; +} +declare module 'shared/models/network-object.model' { + import { Container } from 'shared/utils/util'; + import { Dispose, OnDidDispose, CannotHaveOnDidDispose } from 'shared/models/disposal.model'; + /** + * An object of this type is returned from {@link networkObjectService.get}. + * + * @see networkObjectService + */ + export type NetworkObject = T & OnDidDispose; + /** + * An object of this type is returned from {@link networkObjectService.set}. + * + * @see networkObjectService + */ + export type DisposableNetworkObject = T & OnDidDispose & Dispose; + /** + * An object of this type is passed into {@link networkObjectService.set}. + * + * @see networkObjectService + */ + export type NetworkableObject = 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. + * + * @see networkObjectService + * + * @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. + */ + export type LocalObjectToProxyCreator = ( + id: string, + networkObjectContainer: Container>>, + ) => Partial; +} +declare module 'shared/services/network-object.service' { + import { + NetworkObject, + DisposableNetworkObject, + NetworkableObject, + LocalObjectToProxyCreator, + } from 'shared/models/network-object.model'; + /** + * 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}. + * + * 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: { + initialize: () => Promise; + has: (id: string) => Promise; + get: ( + id: string, + createLocalObjectToProxy?: LocalObjectToProxyCreator | undefined, + ) => Promise | undefined>; + set: ( + id: string, + objectToShare: NetworkableObject, + ) => Promise>; + }; + export default networkObjectService; +} +declare module 'shared/services/data-provider.service' { + /** + * Handles registering data providers and serving data around the papi. + * Exposed on the papi. + */ + import { DataProvider, DisposableDataProvider } from 'shared/models/data-provider.model'; + import IDataProviderEngine from 'shared/models/data-provider-engine.model'; + import { NetworkableObject } from 'shared/models/network-object.model'; + /** Determine if a data provider with the given name exists anywhere on the network */ + function has(providerName: string): Promise; + /** + * 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 + * + * WARNING: registering a dataProviderEngine mutates the provided object. + * Its `notifyUpdate` and `set` methods are layered over to facilitate data provider subscriptions. + * @returns information about 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. + * @type `TSelector` - the type of selector used to get some data from this provider. + * A selector is an object a caller provides to the data provider to tell the provider what subset of data it wants. + * Note: A selector must be stringifiable. + * @type `TData` - the type of data provided by this data provider based on a provided selector + */ + function registerEngine( + providerName: string, + dataProviderEngine: NetworkableObject>, + ): Promise>; + /** + * Get a data provider that has previously been set up + * @param dataProviderName Name of the desired data provider + * @returns The data provider with the given name if one exists, undefined otherwise + */ + function get>( + dataProviderName: string, + ): Promise; + const dataProviderService: { + has: typeof has; + registerEngine: typeof registerEngine; + get: typeof get; + }; + export default dataProviderService; +} +declare module 'renderer/components/papi-components/button.component' { + import { MouseEventHandler, PropsWithChildren } from 'react'; + import 'renderer/components/papi-components/button.component.css'; + export type ButtonProps = PropsWithChildren<{ + /** + * Enabled status of button + * @default false + */ + isDisabled?: boolean; + /** + * Additional css classes to help with unique styling of the button + */ + className?: string; + /** + * Optional click handler + */ + onClick?: MouseEventHandler; + /** + * Optional context menu handler + */ + onContextMenu?: MouseEventHandler; + }>; + /** + * Button a user can click to do something + * + * Thanks to MUI for heavy inspiration and documentation + * https://mui.com/material-ui/getting-started/overview/ + */ + function Button({ + isDisabled, + className, + onClick, + onContextMenu, + children, + }: ButtonProps): JSX.Element; + export default Button; +} +declare module 'renderer/components/papi-components/combo-box.component' { + import { AutocompleteChangeDetails, AutocompleteChangeReason } from '@mui/material'; + import { FocusEventHandler, SyntheticEvent } from 'react'; + import 'renderer/components/papi-components/combo-box.component.css'; + export type ComboBoxChangeDetails = AutocompleteChangeDetails; + export type ComboBoxChangeReason = AutocompleteChangeReason; + export type ComboBoxProps = { + /** + * Text label title for combobox + */ + title?: string; + /** + * If `true`, the component is disabled. + * @default false + */ + isDisabled?: boolean; + /** + * True when (input related to) switch is erroneous + * @default false + */ + hasError?: boolean; + /** + * If `true`, the input will take up the full width of its container. + * @default false + */ + isFullWidth?: boolean; + /** + * List of available options for the dropdown menu + */ + options?: readonly ( + | string + | { + label: string; + } + )[]; + /** + * Additional css classes to help with unique styling of the combo box + */ + className?: string; + /** + * Triggers when content of textfield is changed + */ + onChange?: ( + event: SyntheticEvent, + value: unknown, + reason: ComboBoxChangeReason, + details?: ComboBoxChangeDetails | undefined, + ) => void; + /** + * Triggers when textfield gets focus + */ + onFocus?: FocusEventHandler; + /** + * Triggers when textfield loses focus + */ + onBlur?: FocusEventHandler; + }; + /** + * Dropdown selector displaying various options from which to choose + * + * Thanks to MUI for heavy inspiration and documentation + * https://mui.com/material-ui/getting-started/overview/ + */ + function ComboBox({ + title, + isDisabled, + hasError, + isFullWidth, + options, + className, + onChange, + onFocus, + onBlur, + }: ComboBoxProps): JSX.Element; + export default ComboBox; +} +declare module 'renderer/components/papi-components/slider.component' { + import { SyntheticEvent } from 'react'; + import 'renderer/components/papi-components/slider.component.css'; + export type SliderProps = { + /** + * If `true`, the component is disabled. + * @default false + */ + isDisabled?: boolean; + /** + * The component orientation. + * @default 'horizontal' + */ + orientation?: 'horizontal' | 'vertical'; + /** + * The minimum allowed value of the slider. + * Should not be equal to max. + * @default 0 + */ + min?: number; + /** + * The maximum allowed value of the slider. + * Should not be equal to min. + * @default 100 + */ + max?: number; + /** + * The granularity with which the slider can step through values. (A "discrete" slider.) + * The `min` prop serves as the origin for the valid values. + * We recommend (max - min) to be evenly divisible by the step. + * @default 1 + */ + step?: number; + /** + * Marks indicate predetermined values to which the user can move the slider. + * If `true` the marks are spaced according the value of the `step` prop. + * @default false + */ + showMarks?: boolean; + /** + * The default value. Use when the component is not controlled. + */ + defaultValue?: number; + /** + * Controls when the value label is displayed: + * + * - `auto` the value label will display when the thumb is hovered or focused. + * - `on` will display persistently. + * - `off` will never display. + * @default 'off' + */ + valueLabelDisplay?: 'on' | 'auto' | 'off'; + /** + * Additional css classes to help with unique styling of the button + */ + className?: string; + /** + * Callback function that is fired when the slider's value changed. + * @param event The event source of the callback. You can pull out the new value by accessing event.target.value (any). + * Warning: This is a generic event not a change event. + * @param value The new value. + * @param activeThumb Index of the currently moved thumb. + */ + onChange?: (event: Event, value: number | number[], activeThumb: number) => void; + /** + * Callback function that is fired when the mouseup is triggered. + * @param event The event source of the callback. Warning: This is a generic event not a change event. + * @param value The new value. + */ + onChangeCommitted?: ( + event: Event | SyntheticEvent, + value: number | number[], + ) => void; + }; + /** + * Slider that allows selecting a value from a range + * + * Thanks to MUI for heavy inspiration and documentation + * https://mui.com/material-ui/getting-started/overview/ + */ + function Slider({ + isDisabled, + orientation, + min, + max, + step, + showMarks, + defaultValue, + valueLabelDisplay, + className, + onChange, + onChangeCommitted, + }: SliderProps): JSX.Element; + export default Slider; +} +declare module 'renderer/components/papi-components/switch.component' { + import { ChangeEvent } from 'react'; + import 'renderer/components/papi-components/switch.component.css'; + export type SwitchProps = { + /** + * If `true`, the component is checked. + */ + isChecked?: boolean; + /** + * Enabled status of switch + * @default false + */ + isDisabled?: boolean; + /** + * True when (input related to) switch is erroneous + * @default false + */ + hasError?: boolean; + /** + * Additional css classes to help with unique styling of the switch + */ + className?: string; + /** + * Callback fired when the state is changed. + * @param event The event source of the callback. You can pull out the new value by accessing event.target.value (string). + * You can pull out the new checked state by accessing event.target.checked (boolean). + */ + onChange?: (event: ChangeEvent) => void; + }; + /** + * Switch to toggle on and off + * + * Thanks to MUI for heavy inspiration and documentation + * https://mui.com/material-ui/getting-started/overview/ + */ + function Switch({ + isChecked: checked, + isDisabled, + hasError, + className, + onChange, + }: SwitchProps): JSX.Element; + export default Switch; +} +declare module 'renderer/components/papi-components/text-field.component' { + import { ChangeEventHandler, FocusEventHandler } from 'react'; + export type TextFieldProps = { + /** + * The variant to use. + * @default 'outlined' + */ + variant?: 'outlined' | 'filled'; + /** + * If `true`, the component is disabled. + * @default false + */ + isDisabled?: boolean; + /** + * If `true`, the label is displayed in an error state. + * @default false + */ + hasError?: boolean; + /** + * If `true`, the input will take up the full width of its container. + * @default false + */ + isFullWidth?: boolean; + /** + * Text that gives the user instructions on what contents the TextField expects + */ + helperText?: string; + /** + * The title of the TextField + */ + label?: string; + /** + * The short hint displayed in the `input` before the user enters a value. + */ + placeholder?: string; + /** + * If `true`, the label is displayed as required and the `input` element is required. + * @default false + */ + isRequired?: boolean; + /** + * Additional css classes to help with unique styling of the button + */ + className?: string; + /** + * Starting value for the text field if it is not controlled + */ + defaultValue?: unknown; + /** + * Value of the text field if controlled + */ + value?: unknown; + /** + * Triggers when content of textfield is changed + */ + onChange?: ChangeEventHandler; + /** + * Triggers when textfield gets focus + */ + onFocus?: FocusEventHandler; + /** + * Triggers when textfield loses focus + */ + onBlur?: FocusEventHandler; + }; + /** + * Text input field + * + * Thanks to MUI for heavy inspiration and documentation + * https://mui.com/material-ui/getting-started/overview/ + */ + function TextField({ + variant, + isDisabled, + hasError, + isFullWidth, + helperText, + label, + placeholder, + isRequired, + className, + defaultValue, + value, + onChange, + onFocus, + onBlur, + }: TextFieldProps): JSX.Element; + export default TextField; +} +declare module 'renderer/components/papi-components/index' { + import Button from 'renderer/components/papi-components/button.component'; + import ComboBox from 'renderer/components/papi-components/combo-box.component'; + import Slider from 'renderer/components/papi-components/slider.component'; + import Switch from 'renderer/components/papi-components/switch.component'; + import TextField from 'renderer/components/papi-components/text-field.component'; + /** All React components to be exposed on the papi */ + const papiComponents: { + Button: typeof Button; + ComboBox: typeof ComboBox; + Slider: typeof Slider; + Switch: typeof Switch; + TextField: typeof TextField; + }; + export default papiComponents; + export type PapiComponents = typeof papiComponents; +} +declare module 'renderer/context/papi-context/test.context' { + const TestContext: import('react').Context; + export default TestContext; +} +declare module 'renderer/context/papi-context/index' { + /** All React contexts to be exposed on the papi */ + const papiContext: { + TestContext: import('react').Context; + }; + export default papiContext; + export type PapiContext = typeof papiContext; +} +declare module 'renderer/hooks/papi-hooks/use-promise.hook' { + /** + * Awaits a promise and returns a loading value while the promise is unresolved + * @param promiseFactoryCallback a function that returns the promise to await. If the promise resolves to null, the value will not change. + * If this callback is undefined, the current value will be returned (defaultValue unless it was previously changed and preserveValue is true), and there will be no loading. + * + * WARNING: MUST BE STABLE - const or wrapped in useCallback. The reference must not be updated every render + * @param defaultValue the initial value to return while first awaiting the promise. If preserveValue is false, this value is also shown while awaiting the promise on subsequent calls. + * + * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render + * @param preserveValue whether to leave the value as the most recent resolved promise value or set it back to defaultValue while running the promise again. Default to true + * @returns [value, isLoading] + * - `value`: the current value for the promise, either the defaultValue or the resolved promise value + * - `isLoading`: whether the promise is waiting to be resolved + */ + const usePromise: ( + promiseFactoryCallback: (() => Promise) | undefined, + defaultValue: T, + preserveValue?: boolean, + ) => [value: T, isLoading: boolean]; + export default usePromise; +} +declare module 'renderer/hooks/papi-hooks/use-event.hook' { + import { PapiEvent, PapiEventHandler } from 'shared/models/papi-event.model'; + /** + * Adds an event handler to an event so the event handler runs when the event is emitted + * @param event the event to subscribe to. Can be either a string or an Event + * - If event is a `string`, the network event associated with this type will automatically be used + * - If event is a `PapiEvent`, that event will be used + * - If event is undefined, the callback will not be subscribed. Useful if the event is not yet available for example + * @param eventHandler the callback to run when the event is emitted + * + * WARNING: MUST BE STABLE - const or wrapped in useCallback. The reference must not be updated every render + */ + const useEvent: ( + event: string | PapiEvent | undefined, + eventHandler: PapiEventHandler, + ) => void; + export default useEvent; +} +declare module 'renderer/hooks/papi-hooks/use-event-async.hook' { + import { PapiEvent, PapiEventAsync, PapiEventHandler } from 'shared/models/papi-event.model'; + /** + * Adds an event handler to an asynchronously subscribing/unsubscribing event so the event handler runs when the event is emitted + * @param event the asynchronously (un)subscribing event to subscribe to. Can be either a string or an Event + * - If event is a `string`, the network event associated with this type will automatically be used + * - If event is a `PapiEvent` or `PapiEventAsync`, that event will be used + * - If event is undefined, the callback will not be subscribed. Useful if the event is not yet available for example + * @param eventHandler the callback to run when the event is emitted + * + * WARNING: MUST BE STABLE - const or wrapped in useCallback. The reference must not be updated every render + */ + const useEventAsync: ( + event: string | PapiEvent | PapiEventAsync | undefined, + eventHandler: PapiEventHandler, + ) => void; + export default useEventAsync; +} +declare module 'renderer/hooks/papi-hooks/use-data-provider.hook' { + import { DataProvider } from 'shared/models/data-provider.model'; + /** + * Gets a data provider with specified name + * @param providerName name of the data provider to get + * @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 + * + * @type `T` - the type of data provider to return. Use `IDataProvider`, + * specifying your own types, or provide a custom data provider type + */ + function useDataProvider>( + providerName: string | undefined, + ): T | undefined; + /** + * Passes the provided data provider through. Used to support hooks that already have a dataProvider + * @param 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 + * + * @type `T` - the type of data provider to return. Use `IDataProvider`, + * specifying your own types, or provide a custom data provider type + */ + function useDataProvider>( + dataProvider: T | undefined, + ): T | undefined; + /** + * Gets a data provider with specified provider name + * @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 + * + * @type `T` - the type of data provider to return. Use `IDataProvider`, + * specifying your own types, or provide a custom data provider type + */ + function useDataProvider>( + dataProviderSource: string | T | undefined, + ): T | undefined; + export default useDataProvider; +} +declare module 'renderer/hooks/papi-hooks/use-data.hook' { + import { DataProviderSubscriberOptions } from 'shared/models/data-provider.interface'; + import { DataProvider } from 'shared/models/data-provider.model'; + /** + * Subscribes to run a callback on a data provider's data with specified selector + * @param providerName name of the data provider to subscribe to + * @param selector tells the provider what data this listener is listening for + * @param defaultValue the initial value to return while first awaiting the data + * + * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render + * @param subscriberOptions various options to adjust how the subscriber emits updates + * + * WARNING: If provided, MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render + * @returns [data, setData, isLoading] + * - `data`: the current value for the data from the data provider with the specified selector, either the defaultValue or the resolved data + * - `setData`: asynchronous function to request that the data provider update the data at this 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 selector is awaiting retrieval from the data provider + */ + function useData( + providerName: string | undefined, + selector: TSelector, + defaultValue: TGetData, + subscriberOptions?: DataProviderSubscriberOptions, + ): [TGetData, ((newData: TSetData) => Promise) | undefined, boolean]; + /** + * Subscribes to run a callback on a data provider's data with specified selector + * @param 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 + * @param defaultValue the initial value to return while first awaiting the data + * + * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render + * @param subscriberOptions various options to adjust how the subscriber emits updates + * + * WARNING: If provided, MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render + * @returns [data, setData, isLoading] + * - `data`: the current value for the data from the data provider with the specified selector, either the defaultValue or the resolved data + * - `setData`: asynchronous function to request that the data provider update the data at this 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 selector is awaiting retrieval from the data provider + */ + function useData( + dataProvider: DataProvider | undefined, + selector: TSelector, + defaultValue: TGetData, + subscriberOptions?: DataProviderSubscriberOptions, + ): [TGetData, ((newData: TSetData) => Promise) | undefined, boolean]; + export default useData; +} +declare module 'renderer/hooks/papi-hooks/index' { + import useDataProvider from 'renderer/hooks/papi-hooks/use-data-provider.hook'; + import useData from 'renderer/hooks/papi-hooks/use-data.hook'; + /** All React hooks to be exposed on the papi */ + const papiHooks: { + usePromise: ( + promiseFactoryCallback: (() => Promise) | undefined, + defaultValue: T, + preserveValue?: boolean, + ) => [value: T, isLoading: boolean]; + useEvent: ( + event: string | import('shared/models/papi-event.model').PapiEvent | undefined, + eventHandler: import('shared/models/papi-event.model').PapiEventHandler, + ) => void; + useEventAsync: ( + event: + | string + | import('shared/models/papi-event.model').PapiEvent + | import('shared/models/papi-event.model').PapiEventAsync + | undefined, + eventHandler: import('shared/models/papi-event.model').PapiEventHandler, + ) => void; + useDataProvider: typeof useDataProvider; + useData: typeof useData; + }; + export default papiHooks; + export type PapiHooks = typeof papiHooks; +} +declare module 'papi' { + /** + * Unified module for accessing API features in extensions. + * + * WARNING: DO NOT IMPORT papi IN ANY FILE THAT papi IMPORTS AND EXPOSES. + */ + import * as commandService from 'shared/services/command.service'; + import * as papiUtil from 'shared/utils/papi-util'; + import * as webViewService from 'shared/services/web-view.service'; + import PapiEventEmitter from 'shared/models/papi-event-emitter.model'; + const papi: { + EventEmitter: typeof PapiEventEmitter; + fetch: typeof fetch; + commands: typeof commandService; + util: typeof papiUtil; + webViews: typeof webViewService; + react: { + components: { + Button: typeof import('renderer/components/papi-components/button.component').default; + ComboBox: typeof import('renderer/components/papi-components/combo-box.component').default; + Slider: typeof import('renderer/components/papi-components/slider.component').default; + Switch: typeof import('renderer/components/papi-components/switch.component').default; + TextField: typeof import('renderer/components/papi-components/text-field.component').default; + }; + context: { + TestContext: import('react').Context; + }; + hooks: { + usePromise: ( + promiseFactoryCallback: (() => Promise) | undefined, + defaultValue: T, + preserveValue?: boolean, + ) => [value: T, isLoading: boolean]; + useEvent: ( + event: string | import('shared/models/papi-event.model').PapiEvent | undefined, + eventHandler: import('shared/models/papi-event.model').PapiEventHandler, + ) => void; + useEventAsync: ( + event: + | string + | import('shared/models/papi-event.model').PapiEvent + | import('shared/models/papi-event.model').PapiEventAsync + | undefined, + eventHandler: import('shared/models/papi-event.model').PapiEventHandler, + ) => void; + useDataProvider: typeof import('renderer/hooks/papi-hooks/use-data-provider.hook').default; + useData: typeof import('renderer/hooks/papi-hooks/use-data.hook').default; + }; + }; + network: { + onDidClientConnect: import('shared/models/papi-event.model').PapiEvent< + import('shared/data/internal-connection.model').ClientConnectEvent + >; + onDidClientDisconnect: import('shared/models/papi-event.model').PapiEvent< + import('shared/data/internal-connection.model').ClientDisconnectEvent + >; + createNetworkEventEmitter: (eventType: string) => PapiEventEmitter; + getNetworkEvent: ( + eventType: string, + ) => import('shared/models/papi-event.model').PapiEvent; + }; + logger: import('electron-log').Logger & { + default: import('electron-log').Logger; + }; + internet: { + fetch: typeof fetch; + }; + dataProvider: { + has: (providerName: string) => Promise; + registerEngine: ( + providerName: string, + dataProviderEngine: import('shared/models/network-object.model').NetworkableObject< + import('shared/models/data-provider-engine.model').default + >, + ) => Promise< + import('shared/models/data-provider.model').DisposableDataProvider< + TSelector, + TGetData, + TSetData + > + >; + get: >( + dataProviderName: string, + ) => Promise; + }; + }; + export default papi; + /** + * 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 | undefined; + }>; +} From ba1607cda65ad8874ca301088a7ba0984db59911 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 28 Apr 2023 13:42:34 -0500 Subject: [PATCH 2/4] Adjusted network object and data provider types to indicate their constraints and relationships more strongly --- .../hello-someone/hello-someone.web-view.ejs | 5 +- lib/papi-dts/papi.d.ts | 272 ++++++++++-------- .../models/data-provider-engine.model.ts | 6 +- src/shared/models/data-provider.interface.ts | 3 +- src/shared/models/data-provider.model.ts | 14 +- src/shared/models/disposal.model.ts | 7 +- src/shared/models/network-object.model.ts | 32 ++- src/shared/services/data-provider.service.ts | 63 ++-- src/shared/services/network-object.service.ts | 36 +-- 9 files changed, 258 insertions(+), 180 deletions(-) diff --git a/extensions/lib/hello-someone/hello-someone.web-view.ejs b/extensions/lib/hello-someone/hello-someone.web-view.ejs index 4560386515..6a463ff698 100644 --- a/extensions/lib/hello-someone/hello-someone.web-view.ejs +++ b/extensions/lib/hello-someone/hello-someone.web-view.ejs @@ -63,7 +63,10 @@ ); const helloSomeoneOutput = document.getElementById('greetings-button-output'); helloSomeoneOutput.textContent = papi.util.htmlEncode( - `${success ? 'Successfully' : 'Unsuccessfully'} set ${name}'s greeting!`, + `${success ? 'Successfully' : 'Unsuccessfully'} set ${name}'s greeting!`.replace( + /'/g, + '`', + ), ); }); diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index a52f0b2f4c..66ec9976ca 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -1174,9 +1174,136 @@ declare module 'shared/services/internet.service' { }; export default internetService; } +declare module 'shared/models/disposal.model' { + import { PapiEvent } from 'shared/models/papi-event.model'; + import { UnsubscriberAsync } from 'shared/utils/papi-util'; + /** Require a `dispose` function */ + export interface Dispose { + /** Release resources and notify dependent services when tearing down an object */ + dispose: UnsubscriberAsync; + } + /** Require an `onDidDispose` event */ + export interface OnDidDispose { + /** Event that emits when `dispose` is called on an object */ + onDidDispose: PapiEvent; + } + /** Indicates than an object cannot have an `onDidDispose` event. + * Also allows an object to include a `dispose` function. */ + export interface CannotHaveOnDidDispose { + /** Release resources and notify dependent services when tearing down an object */ + dispose?: UnsubscriberAsync; + /** Event that emits when `dispose` is called on an object */ + onDidDispose?: undefined; + } + /** Allow onDidDispose to exist on the type if it was previously disallowed by CannotHaveOnDidDispose */ + export type CanHaveOnDidDispose = Omit; +} +declare module 'shared/services/network-object.service' { + import { + NetworkObject, + DisposableNetworkObject, + NetworkableObject, + LocalObjectToProxyCreator, + } from 'shared/models/network-object.model'; + /** + * 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}. + * + * 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: { + initialize: () => Promise; + has: (id: string) => Promise; + get: ( + id: string, + createLocalObjectToProxy?: LocalObjectToProxyCreator | undefined, + ) => Promise | undefined>; + set: >( + id: string, + objectToShare: T_1, + ) => Promise>; + }; + export default networkObjectService; +} +declare module 'shared/models/network-object.model' { + import { Container } from 'shared/utils/util'; + import { + Dispose, + OnDidDispose, + CannotHaveOnDidDispose, + CanHaveOnDidDispose, + } from 'shared/models/disposal.model'; + /** + * 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. + * + * @see networkObjectService + */ + export type NetworkObject = CanHaveOnDidDispose & 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. + * + * @see networkObjectService + * + * @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 + */ + export type LocalObjectToProxyCreator = ( + id: string, + networkObjectContainer: Container>, + ) => Partial; +} declare module 'shared/models/data-provider.interface' { import { UnsubscriberAsync } from 'shared/utils/papi-util'; import { PapiEventHandler } from 'shared/models/papi-event.model'; + import { NetworkableObject } from 'shared/models/network-object.model'; /** Various options to adjust how the data provider subscriber emits updates */ export type DataProviderSubscriberOptions = { /** @@ -1228,7 +1355,7 @@ declare module 'shared/models/data-provider.interface' { * @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 */ - interface IDataProvider { + interface IDataProvider extends NetworkableObject { /** * Set a subset of data according to the selector. * @@ -1262,48 +1389,34 @@ declare module 'shared/models/data-provider.interface' { } export default IDataProvider; } -declare module 'shared/models/disposal.model' { - import { PapiEvent } from 'shared/models/papi-event.model'; - import { UnsubscriberAsync } from 'shared/utils/papi-util'; - /** Require a `dispose` function */ - export interface Dispose { - /** Release resources and notify dependent services when tearing down an object */ - dispose: UnsubscriberAsync; - } - /** Require an `onDidDispose` event */ - export interface OnDidDispose { - /** Event that emits when `dispose` is called on an object */ - onDidDispose: PapiEvent; - } - /** Indicates than an object cannot have an `onDidDispose` event. - * Also allows an object to include a `dispose` function. */ - export type CannotHaveOnDidDispose = T & { - /** Release resources and notify dependent services when tearing down an object */ - dispose?: UnsubscriberAsync; - /** Event that emits when `dispose` is called on an object */ - onDidDispose?: undefined; - }; -} declare module 'shared/models/data-provider.model' { import IDataProvider from 'shared/models/data-provider.interface'; - import { Dispose, OnDidDispose } from 'shared/models/disposal.model'; + import { + DisposableNetworkObject, + NetworkObject, + NetworkableObject, + } from 'shared/models/network-object.model'; + import { CanHaveOnDidDispose } from 'shared/models/disposal.model'; /** * Information about a data provider. * Returned from getting a data provider. */ - export type DataProvider = OnDidDispose & - IDataProvider; + export interface DataProvider + extends NetworkObject, + CanHaveOnDidDispose> {} /** * Information about a data provider including control over disposing of it. * Returned from registering a data provider (only the process that set it up should dispose of it) */ - export type DisposableDataProvider = Dispose & - DataProvider; + export interface DisposableDataProvider + extends DisposableNetworkObject, + Omit, 'dispose'> {} } declare module 'shared/models/data-provider-engine.model' { + import { NetworkableObject } from 'shared/models/network-object.model'; /** * The object to register with the DataProviderService to create a data provider. - * The DataProviderService creates a IDataProvider on the papi that layers over this engine, providing special functionality + * The DataProviderService creates an IDataProvider on the papi that layers over this engine, providing special functionality * * Note: methods on objects that implement this interface must be unbound functions, not arrow functions. * @type `TSelector` - the type of selector used to get some data from this provider. @@ -1311,7 +1424,7 @@ declare module 'shared/models/data-provider-engine.model' { * @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 */ - interface IDataProviderEngine { + interface IDataProviderEngine extends NetworkableObject { /** * Method to run to send clients updates outside of the `set` method. * papi overwrites this function on the DataProviderEngine itself to emit an update after running the defined `notifyUpdate` method in the DataProviderEngine. @@ -1343,96 +1456,6 @@ declare module 'shared/models/data-provider-engine.model' { } export default IDataProviderEngine; } -declare module 'shared/models/network-object.model' { - import { Container } from 'shared/utils/util'; - import { Dispose, OnDidDispose, CannotHaveOnDidDispose } from 'shared/models/disposal.model'; - /** - * An object of this type is returned from {@link networkObjectService.get}. - * - * @see networkObjectService - */ - export type NetworkObject = T & OnDidDispose; - /** - * An object of this type is returned from {@link networkObjectService.set}. - * - * @see networkObjectService - */ - export type DisposableNetworkObject = T & OnDidDispose & Dispose; - /** - * An object of this type is passed into {@link networkObjectService.set}. - * - * @see networkObjectService - */ - export type NetworkableObject = 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. - * - * @see networkObjectService - * - * @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. - */ - export type LocalObjectToProxyCreator = ( - id: string, - networkObjectContainer: Container>>, - ) => Partial; -} -declare module 'shared/services/network-object.service' { - import { - NetworkObject, - DisposableNetworkObject, - NetworkableObject, - LocalObjectToProxyCreator, - } from 'shared/models/network-object.model'; - /** - * 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}. - * - * 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: { - initialize: () => Promise; - has: (id: string) => Promise; - get: ( - id: string, - createLocalObjectToProxy?: LocalObjectToProxyCreator | undefined, - ) => Promise | undefined>; - set: ( - id: string, - objectToShare: NetworkableObject, - ) => Promise>; - }; - export default networkObjectService; -} declare module 'shared/services/data-provider.service' { /** * Handles registering data providers and serving data around the papi. @@ -1440,7 +1463,6 @@ declare module 'shared/services/data-provider.service' { */ import { DataProvider, DisposableDataProvider } from 'shared/models/data-provider.model'; import IDataProviderEngine from 'shared/models/data-provider-engine.model'; - import { NetworkableObject } from 'shared/models/network-object.model'; /** Determine if a data provider with the given name exists anywhere on the network */ function has(providerName: string): Promise; /** @@ -1459,7 +1481,7 @@ declare module 'shared/services/data-provider.service' { */ function registerEngine( providerName: string, - dataProviderEngine: NetworkableObject>, + dataProviderEngine: IDataProviderEngine, ): Promise>; /** * Get a data provider that has previously been set up @@ -2096,8 +2118,10 @@ declare module 'papi' { has: (providerName: string) => Promise; registerEngine: ( providerName: string, - dataProviderEngine: import('shared/models/network-object.model').NetworkableObject< - import('shared/models/data-provider-engine.model').default + dataProviderEngine: import('shared/models/data-provider-engine.model').default< + TSelector, + TGetData, + TSetData >, ) => Promise< import('shared/models/data-provider.model').DisposableDataProvider< diff --git a/src/shared/models/data-provider-engine.model.ts b/src/shared/models/data-provider-engine.model.ts index 4f478752bf..cc988f0a10 100644 --- a/src/shared/models/data-provider-engine.model.ts +++ b/src/shared/models/data-provider-engine.model.ts @@ -1,6 +1,8 @@ +import { NetworkableObject } from './network-object.model'; + /** * The object to register with the DataProviderService to create a data provider. - * The DataProviderService creates a IDataProvider on the papi that layers over this engine, providing special functionality + * The DataProviderService creates an IDataProvider on the papi that layers over this engine, providing special functionality * * Note: methods on objects that implement this interface must be unbound functions, not arrow functions. * @type `TSelector` - the type of selector used to get some data from this provider. @@ -8,7 +10,7 @@ * @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 */ -interface IDataProviderEngine { +interface IDataProviderEngine extends NetworkableObject { /** * Method to run to send clients updates outside of the `set` method. * papi overwrites this function on the DataProviderEngine itself to emit an update after running the defined `notifyUpdate` method in the DataProviderEngine. diff --git a/src/shared/models/data-provider.interface.ts b/src/shared/models/data-provider.interface.ts index a1495defdf..7ec97ba37c 100644 --- a/src/shared/models/data-provider.interface.ts +++ b/src/shared/models/data-provider.interface.ts @@ -1,5 +1,6 @@ import { UnsubscriberAsync } from '@shared/utils/papi-util'; import { PapiEventHandler } from '@shared/models/papi-event.model'; +import { NetworkableObject } from './network-object.model'; /** Various options to adjust how the data provider subscriber emits updates */ export type DataProviderSubscriberOptions = { @@ -54,7 +55,7 @@ export type DataProviderSubscriber = ( * @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 */ -interface IDataProvider { +interface IDataProvider extends NetworkableObject { /** * Set a subset of data according to the selector. * diff --git a/src/shared/models/data-provider.model.ts b/src/shared/models/data-provider.model.ts index 0e46cd43d0..cd85258edb 100644 --- a/src/shared/models/data-provider.model.ts +++ b/src/shared/models/data-provider.model.ts @@ -1,18 +1,22 @@ import IDataProvider from '@shared/models/data-provider.interface'; -import { Dispose, OnDidDispose } from './disposal.model'; +import { DisposableNetworkObject, NetworkObject, NetworkableObject } from './network-object.model'; +import { CanHaveOnDidDispose } from './disposal.model'; /** * Information about a data provider. * Returned from getting a data provider. */ // Basically a layer over NetworkObject -export type DataProvider = OnDidDispose & - IDataProvider; +export interface DataProvider + extends NetworkObject, + CanHaveOnDidDispose> {} /** * Information about a data provider including control over disposing of it. * Returned from registering a data provider (only the process that set it up should dispose of it) */ // Basically a layer over DisposableNetworkObject -export type DisposableDataProvider = Dispose & - DataProvider; +export interface DisposableDataProvider + extends DisposableNetworkObject, + // Need to omit dispose here because it is optional on DataProvider but is required on DisposableNetworkObject + Omit, 'dispose'> {} diff --git a/src/shared/models/disposal.model.ts b/src/shared/models/disposal.model.ts index 3c38055240..91e38eef0f 100644 --- a/src/shared/models/disposal.model.ts +++ b/src/shared/models/disposal.model.ts @@ -15,10 +15,13 @@ export interface OnDidDispose { /** Indicates than an object cannot have an `onDidDispose` event. * Also allows an object to include a `dispose` function. */ -export type CannotHaveOnDidDispose = T & { +export interface CannotHaveOnDidDispose { /** Release resources and notify dependent services when tearing down an object */ // Include dispose optionally to avoid extra type casting later dispose?: UnsubscriberAsync; /** Event that emits when `dispose` is called on an object */ onDidDispose?: undefined; -}; +} + +/** Allow onDidDispose to exist on the type if it was previously disallowed by CannotHaveOnDidDispose */ +export type CanHaveOnDidDispose = Omit; diff --git a/src/shared/models/network-object.model.ts b/src/shared/models/network-object.model.ts index c125521999..6c5501dc10 100644 --- a/src/shared/models/network-object.model.ts +++ b/src/shared/models/network-object.model.ts @@ -1,26 +1,37 @@ import { Container } from '@shared/utils/util'; -import { Dispose, OnDidDispose, CannotHaveOnDidDispose } from './disposal.model'; +// This is used in @see comments in the JSDoc +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type networkObjectService from '@shared/services/network-object.service'; +import { + Dispose, + OnDidDispose, + CannotHaveOnDidDispose, + CanHaveOnDidDispose, +} from './disposal.model'; /** * 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. + * * @see networkObjectService */ -export type NetworkObject = T & OnDidDispose; +export type NetworkObject = CanHaveOnDidDispose & OnDidDispose; /** * An object of this type is returned from {@link networkObjectService.set}. * * @see networkObjectService */ -export type DisposableNetworkObject = T & OnDidDispose & Dispose; +export type DisposableNetworkObject = NetworkObject & Dispose; /** * An object of this type is passed into {@link networkObjectService.set}. * * @see networkObjectService */ -export type NetworkableObject = CannotHaveOnDidDispose; +export type NetworkableObject = T & CannotHaveOnDidDispose; /** * If a network object with the provided ID exists remotely but has not been set up to use inside @@ -42,8 +53,17 @@ export type NetworkableObject = CannotHaveOnDidDispose; * 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 */ export type LocalObjectToProxyCreator = ( id: string, - networkObjectContainer: Container>>, -) => Partial; + networkObjectContainer: Container>, +) => // TODO: This should be Partial but TypeScript can't infer the type of T. Figure this out. +// For example, in dataProviderService.get, it uses createLocalDataProviderToProxy (which does +// return Partial and matches signature with this function), but it was producing a type error +// because its Partial is different than this signature's Partial even though T is generic +// and extends NetworkableObject in both cases. +Partial; diff --git a/src/shared/services/data-provider.service.ts b/src/shared/services/data-provider.service.ts index 951bdec5ff..88e255bcd0 100644 --- a/src/shared/services/data-provider.service.ts +++ b/src/shared/services/data-provider.service.ts @@ -14,7 +14,7 @@ import PapiEventEmitter from '@shared/models/papi-event-emitter.model'; import * as networkService from '@shared/services/network.service'; import { deepEqual, serializeRequestType } from '@shared/utils/papi-util'; import { Container } from '@shared/utils/util'; -import { NetworkObject, NetworkableObject } from '@shared/models/network-object.model'; +import { NetworkObject } from '@shared/models/network-object.model'; import networkObjectService from '@shared/services/network-object.service'; import logger from './logger.service'; @@ -65,12 +65,12 @@ async function has(providerName: string): Promise { /** * Creates a subscribe function for a data provider to allow subscribing to updates on the data - * @param dataProviderContainer container that holds a reference to the data provider so this subscribe function can reference the data provider + * @param dataProviderContainer container that holds a reference to the network object data provider so this subscribe function can reference the data provider * @param onDidUpdate the event to listen to for updates on the data * @returns subscribe function for a data provider */ function createDataProviderSubscriber( - dataProviderContainer: Container>, + dataProviderContainer: Container>, onDidUpdate: PapiEvent, ): DataProviderSubscriber { return async (selector, callback, options?: DataProviderSubscriberOptions) => { @@ -140,18 +140,15 @@ function createDataProviderSubscriber( * * WARNING: this function mutates the provided object. Its `notifyUpdate` and `set` methods are layered over to facilitate data provider subscriptions. * @param dataProviderEngine provider engine that handles setting and getting data as well as informing which listeners should get what updates + * @param dataProviderContainer container that holds a reference to the network object data provider so the subscribe function can reference the data provider * @param onDidUpdateEmitter event emitter to use for informing subscribers of updates. The event just returns what set returns (should be true according to IDataProvider) * @returns data provider layering over the provided data provider engine */ function buildDataProvider( dataProviderEngine: IDataProviderEngine, + dataProviderContainer: Container>, onDidUpdateEmitter: PapiEventEmitter, ): IDataProvider { - /** Container to hold a reference to the data provider so the local object can reference the network object in its functions */ - const dataProviderContainer: Container> = { - contents: undefined, - }; - // Layer over data provider engine methods to give it control over emitting updates // Layer over the data provider engine's notifyUpdate with one that actually emits an update // or if the dpe doesn't have notifyUpdate, give it one @@ -190,14 +187,6 @@ function buildDataProvider( subscribe: createDataProviderSubscriber(dataProviderContainer, onDidUpdateEmitter.event), }; - // Update the dataProviderContainer so the local object can access the dataProvider appropriately - // See above for why dataProviderInternal does not have set and why it needs to be type asserted here for now - dataProviderContainer.contents = dataProviderInternal as IDataProvider< - TSelector, - TGetData, - TSetData - >; - // Create a proxy that runs the data provider method if it exists or runs the engine method otherwise const dataProvider = new Proxy(dataProviderEngine, { get(obj: IDataProviderEngine, prop) { @@ -236,7 +225,7 @@ function buildDataProvider( Reflect.set(obj, prop, value); return true; }, - // Type assert the data provider engine proxy because it is an IDataProvider although + // Type assert the data provider engine proxy because it is a DataProviderInternal although // Typescript can't figure it out }) as unknown as IDataProvider; @@ -259,7 +248,7 @@ function buildDataProvider( */ async function registerEngine( providerName: string, - dataProviderEngine: NetworkableObject>, + dataProviderEngine: IDataProviderEngine, ): Promise> { await initialize(); @@ -278,16 +267,42 @@ async function registerEngine( // Get the object id for this data provider name const dataProviderObjectId = getDataProviderObjectId(providerName); + /** Container to hold a reference to the final network object data provider so the local object + * can reference the network object in its functions + */ + const dataProviderContainer: Container> = { + contents: undefined, + }; + // Create a networked update event const onDidUpdateEmitter = networkService.createNetworkEventEmitter( serializeRequestType(dataProviderObjectId, ON_DID_UPDATE), ); // Build the data provider - const dataProvider = buildDataProvider(dataProviderEngine, onDidUpdateEmitter); + const dataProviderInternal = buildDataProvider( + dataProviderEngine, + dataProviderContainer, + onDidUpdateEmitter, + ); // Set up the data provider to be a network object so other processes can use it - return networkObjectService.set(dataProviderObjectId, dataProvider); + const disposableDataProvider = (await networkObjectService.set( + dataProviderObjectId, + dataProviderInternal, + )) as DisposableDataProvider; + + // Get the local network object proxy for the data provider so you can't call + // dataProviderContainer.contents.dispose + const dataProvider = await networkObjectService.get>( + dataProviderObjectId, + ); + + // Update the dataProviderContainer so the internal data provider (specifically its subscribe + // function) can access the dataProvider appropriately + dataProviderContainer.contents = dataProvider; + + return disposableDataProvider; } /** @@ -296,10 +311,12 @@ async function registerEngine( * @param dataProviderContainer container that holds a reference to the data provider so this subscribe function can reference the data provider * @returns local data provider object that represents a remote data provider */ +// This generic type should be DataProviderInternal because we are making part of a local/internal data provider // eslint-disable-next-line @typescript-eslint/no-explicit-any function createLocalDataProviderToProxy>( dataProviderObjectId: string, - dataProviderContainer: Container>>, + // NetworkObject is our way to convert from DataProviderInternal to IDataProvider without specifying generics + dataProviderContainer: Container>, ): Partial { // Create a networked update event const onDidUpdate = networkService.getNetworkEvent( @@ -326,10 +343,10 @@ async function get>( const dataProviderObjectId = getDataProviderObjectId(dataProviderName); // Get the network object for this data provider - const dataProvider = await networkObjectService.get( + const dataProvider = (await networkObjectService.get( dataProviderObjectId, createLocalDataProviderToProxy, - ); + )) as T; if (!dataProvider) { logger.info(`No data provider found with name = ${dataProviderName}`); diff --git a/src/shared/services/network-object.service.ts b/src/shared/services/network-object.service.ts index c2010a70dc..7da08f818d 100644 --- a/src/shared/services/network-object.service.ts +++ b/src/shared/services/network-object.service.ts @@ -16,6 +16,7 @@ import { LocalObjectToProxyCreator, } from '@shared/models/network-object.model'; import { Mutex } from 'async-mutex'; +import { CanHaveOnDidDispose } from '@shared/models/disposal.model'; import logger from './logger.service'; // #endregion @@ -92,7 +93,7 @@ type NetworkObjectRegistration = { /** Local or Remote */ registrationType: NetworkObjectRegistrationType; /** Proxy object shared with services that don't own the actual object - returned by "get" */ - networkObject: NetworkObject; + networkObject: NetworkObject; /** Emitter that indicates locally when the network object was disposed. * Run when the network disposal emitter runs for this registration's ID. */ @@ -311,36 +312,42 @@ const get = async ( // At this point, the object exists remotely but does not yet exist locally. - // The base object created below might need a reference to the final proxy. Since the proxy - // doesn't exist yet, create a container now and fill it in after the proxy is created. - const proxyContainer: Container>> = { + // The base object created below might need a reference to the final network object. Since the + // network object doesn't exist yet, create a container now and fill it in after the network + // object is created. + const proxyContainer: Container> = { contents: undefined, }; // Create the base object that will be proxied for remote calls. // If a property exists on the base object, we use it and won't look for it on the remote object. // If a property does not exist on the base object, it is assumed to exist on the remote object. - const baseObject = createLocalObjectToProxy ? createLocalObjectToProxy(id, proxyContainer) : {}; + const baseObject: Partial = createLocalObjectToProxy + ? (createLocalObjectToProxy(id, proxyContainer) as Partial) + : {}; // Create a proxy with functions that will send requests to the remote object const remoteProxy = createRemoteProxy(id, baseObject); - // Store the proxy in the container so baseObject has a valid reference - proxyContainer.contents = remoteProxy.proxy as NetworkObject; - // Setup onDidDispose so that services will know when the proxy is dead const eventEmitter = new PapiEventEmitter(); overrideOnDidDispose(id, remoteProxy.proxy, eventEmitter.event); + // The network object is finished! Rename it so we know it is finished + const networkObject = remoteProxy.proxy as NetworkObject; + + // Store the network object in the container so baseObject has a valid reference + proxyContainer.contents = networkObject; + // Save the network object for future lookups networkObjectRegistrations.set(id, { registrationType: NetworkObjectRegistrationType.Remote, onDidDisposeEmitter: eventEmitter, - networkObject: remoteProxy.proxy as NetworkObject, + networkObject, revokeProxy: remoteProxy.revoke, }); - return remoteProxy.proxy as NetworkObject; + return networkObject; }); }; @@ -359,9 +366,9 @@ const get = async ( * @returns INetworkObjectDisposer wrapping the object to share */ -const set = async ( +const set = async ( id: string, - objectToShare: NetworkableObject, + objectToShare: T, ): Promise> => { await initialize(); @@ -457,10 +464,7 @@ const set = async ( // Override objectToShare's type's force-undefined onDidDispose to DisposableNetworkObject's // onDidDispose type because it had an onDidDispose added in overrideOnDidDispose. - return objectToShare as Omit< - typeof objectToShare, - 'onDidDispose' - > as DisposableNetworkObject; + return objectToShare as CanHaveOnDidDispose as DisposableNetworkObject; }); }; From 248c990bfecd2f20548819e1030112be3bd7e461 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 28 Apr 2023 14:58:27 -0500 Subject: [PATCH 3/4] Renamed data provider types --- .../lib/hello-someone/hello-someone.d.ts | 4 +- extensions/lib/hello-someone/hello-someone.ts | 2 +- extensions/lib/quick-verse/quick-verse.d.ts | 4 +- lib/papi-dts/papi.d.ts | 67 +++++++++++-------- .../papi-hooks/use-data-provider.hook.ts | 10 +-- .../hooks/papi-hooks/use-data.hook.ts | 8 +-- .../testing/test-buttons-panel.component.tsx | 4 +- src/shared/models/data-provider.interface.ts | 12 ++-- src/shared/models/data-provider.model.ts | 29 +++++--- src/shared/services/data-provider.service.ts | 39 ++++++----- 10 files changed, 102 insertions(+), 77 deletions(-) diff --git a/extensions/lib/hello-someone/hello-someone.d.ts b/extensions/lib/hello-someone/hello-someone.d.ts index c8f5c9a2ef..a9a0f6a6d9 100644 --- a/extensions/lib/hello-someone/hello-someone.d.ts +++ b/extensions/lib/hello-someone/hello-someone.d.ts @@ -1,5 +1,5 @@ -import type { DataProvider } from 'shared/models/data-provider.model'; +import type { IDataProvider } from 'shared/models/data-provider.model'; -export interface GreetingsDataProvider extends DataProvider { +export interface GreetingsDataProvider extends IDataProvider { testRandomMethod(things: string): Promise; } diff --git a/extensions/lib/hello-someone/hello-someone.ts b/extensions/lib/hello-someone/hello-someone.ts index f494a0f8d1..1b5dfa5b58 100644 --- a/extensions/lib/hello-someone/hello-someone.ts +++ b/extensions/lib/hello-someone/hello-someone.ts @@ -1,7 +1,7 @@ import papi from 'papi'; import type { WebViewContentType } from 'shared/data/web-view.model'; -import type IDataProvider from 'shared/models/data-provider.interface'; import { UnsubscriberAsync } from 'shared/utils/papi-util'; +import type { IDataProvider } from 'shared/models/data-provider.model'; // @ts-expect-error ts(1192) this file has no default export; the text is exported by rollup import helloSomeoneHtmlWebView from './hello-someone.web-view.ejs'; diff --git a/extensions/lib/quick-verse/quick-verse.d.ts b/extensions/lib/quick-verse/quick-verse.d.ts index 9528c42e3d..3b588ec9cd 100644 --- a/extensions/lib/quick-verse/quick-verse.d.ts +++ b/extensions/lib/quick-verse/quick-verse.d.ts @@ -1,10 +1,10 @@ -import type { DataProvider } from 'shared/models/data-provider.model'; +import type { IDataProvider } from 'shared/models/data-provider.model'; // TODO: Move these types to quick-verse.ts and generate this file from quick-verse.ts in the future? export type QuickVerseSetData = string | { text: string; isHeresy: boolean }; export interface QuickVerseDataProvider - extends DataProvider { + extends IDataProvider { setHeresy(verseRef: string, verseText: string): Promise; } diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index 66ec9976ca..f55106d36d 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -1347,15 +1347,19 @@ declare module 'shared/models/data-provider.interface' { options?: DataProviderSubscriberOptions, ) => Promise; /** - * 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. + * 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. + * * @type `TSelector` - the type of selector used to get some data from this provider. * A selector is an object a caller provides to the data provider to tell the provider what subset of data it wants. * Note: A selector must be stringifiable. * @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 + * + * @see IDataProvider */ - interface IDataProvider extends NetworkableObject { + interface DataProviderInternal extends NetworkableObject { /** * Set a subset of data according to the selector. * @@ -1387,10 +1391,10 @@ declare module 'shared/models/data-provider.interface' { */ subscribe: DataProviderSubscriber; } - export default IDataProvider; + export default DataProviderInternal; } declare module 'shared/models/data-provider.model' { - import IDataProvider from 'shared/models/data-provider.interface'; + import DataProviderInternal from 'shared/models/data-provider.interface'; import { DisposableNetworkObject, NetworkObject, @@ -1398,19 +1402,28 @@ declare module 'shared/models/data-provider.model' { } from 'shared/models/network-object.model'; import { CanHaveOnDidDispose } from 'shared/models/disposal.model'; /** - * Information about a data provider. - * Returned from getting a data provider. + * 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. + * @type `TSelector` - the type of selector used to get some data from this provider. + * A selector is an object a caller provides to the data provider to tell the provider what subset of data it wants. + * Note: A selector must be stringifiable. + * @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 interface DataProvider + export interface IDataProvider extends NetworkObject, - CanHaveOnDidDispose> {} + CanHaveOnDidDispose> {} /** - * Information about a data provider including control over disposing of it. - * Returned from registering a data provider (only the process that set it up should dispose of it) + * 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 interface DisposableDataProvider + export interface IDisposableDataProvider extends DisposableNetworkObject, - Omit, 'dispose'> {} + Omit, 'dispose'> {} } declare module 'shared/models/data-provider-engine.model' { import { NetworkableObject } from 'shared/models/network-object.model'; @@ -1461,7 +1474,7 @@ declare module 'shared/services/data-provider.service' { * Handles registering data providers and serving data around the papi. * Exposed on the papi. */ - import { DataProvider, DisposableDataProvider } from 'shared/models/data-provider.model'; + import { IDataProvider, IDisposableDataProvider } from 'shared/models/data-provider.model'; import IDataProviderEngine from 'shared/models/data-provider-engine.model'; /** Determine if a data provider with the given name exists anywhere on the network */ function has(providerName: string): Promise; @@ -1482,14 +1495,14 @@ declare module 'shared/services/data-provider.service' { function registerEngine( providerName: string, dataProviderEngine: IDataProviderEngine, - ): Promise>; + ): Promise>; /** * Get a data provider that has previously been set up - * @param dataProviderName Name of the desired data provider + * @param providerName Name of the desired data provider * @returns The data provider with the given name if one exists, undefined otherwise */ - function get>( - dataProviderName: string, + function get>( + providerName: string, ): Promise; const dataProviderService: { has: typeof has; @@ -1927,7 +1940,7 @@ declare module 'renderer/hooks/papi-hooks/use-event-async.hook' { export default useEventAsync; } declare module 'renderer/hooks/papi-hooks/use-data-provider.hook' { - import { DataProvider } from 'shared/models/data-provider.model'; + import { IDataProvider } from 'shared/models/data-provider.model'; /** * Gets a data provider with specified name * @param providerName name of the data provider to get @@ -1938,7 +1951,7 @@ declare module 'renderer/hooks/papi-hooks/use-data-provider.hook' { * @type `T` - the type of data provider to return. Use `IDataProvider`, * specifying your own types, or provide a custom data provider type */ - function useDataProvider>( + function useDataProvider>( providerName: string | undefined, ): T | undefined; /** @@ -1951,7 +1964,7 @@ declare module 'renderer/hooks/papi-hooks/use-data-provider.hook' { * @type `T` - the type of data provider to return. Use `IDataProvider`, * specifying your own types, or provide a custom data provider type */ - function useDataProvider>( + function useDataProvider>( dataProvider: T | undefined, ): T | undefined; /** @@ -1965,14 +1978,14 @@ declare module 'renderer/hooks/papi-hooks/use-data-provider.hook' { * @type `T` - the type of data provider to return. Use `IDataProvider`, * specifying your own types, or provide a custom data provider type */ - function useDataProvider>( + function useDataProvider>( dataProviderSource: string | T | undefined, ): T | undefined; export default useDataProvider; } declare module 'renderer/hooks/papi-hooks/use-data.hook' { import { DataProviderSubscriberOptions } from 'shared/models/data-provider.interface'; - import { DataProvider } from 'shared/models/data-provider.model'; + import { IDataProvider } from 'shared/models/data-provider.model'; /** * Subscribes to run a callback on a data provider's data with specified selector * @param providerName name of the data provider to subscribe to @@ -2012,7 +2025,7 @@ declare module 'renderer/hooks/papi-hooks/use-data.hook' { * - `isLoading`: whether the data with the selector is awaiting retrieval from the data provider */ function useData( - dataProvider: DataProvider | undefined, + dataProvider: IDataProvider | undefined, selector: TSelector, defaultValue: TGetData, subscriberOptions?: DataProviderSubscriberOptions, @@ -2124,14 +2137,14 @@ declare module 'papi' { TSetData >, ) => Promise< - import('shared/models/data-provider.model').DisposableDataProvider< + import('shared/models/data-provider.model').IDisposableDataProvider< TSelector, TGetData, TSetData > >; - get: >( - dataProviderName: string, + get: >( + providerName: string, ) => Promise; }; }; diff --git a/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts b/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts index 821f6159f9..a9a2b4b6fd 100644 --- a/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts @@ -1,5 +1,5 @@ import dataProviderService from '@shared/services/data-provider.service'; -import { DataProvider } from '@shared/models/data-provider.model'; +import { IDataProvider } from '@shared/models/data-provider.model'; import { useCallback, useMemo, useState } from 'react'; import useEvent from '@renderer/hooks/papi-hooks/use-event.hook'; import usePromise from '@renderer/hooks/papi-hooks/use-promise.hook'; @@ -18,7 +18,7 @@ import { isString } from '@shared/utils/util'; // User of this hook must provide types. Cannot use `unknown` here unfortunately because // TypeScript would think we want the implementing IDataProvider types to be unknown as well // eslint-disable-next-line @typescript-eslint/no-explicit-any -function useDataProvider>( +function useDataProvider>( providerName: string | undefined, ): T | undefined; /** @@ -32,7 +32,7 @@ function useDataProvider>( * specifying your own types, or provide a custom data provider type */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -function useDataProvider>( +function useDataProvider>( dataProvider: T | undefined, ): T | undefined; /** @@ -47,11 +47,11 @@ function useDataProvider>( * specifying your own types, or provide a custom data provider type */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -function useDataProvider>( +function useDataProvider>( dataProviderSource: string | T | undefined, ): T | undefined; // eslint-disable-next-line @typescript-eslint/no-explicit-any -function useDataProvider>( +function useDataProvider>( dataProviderSource: string | T | undefined, ): T | undefined { // Check to see if they passed in the results of a useDataProvider hook or undefined diff --git a/src/renderer/hooks/papi-hooks/use-data.hook.ts b/src/renderer/hooks/papi-hooks/use-data.hook.ts index cd0da7be44..b13b84e6a3 100644 --- a/src/renderer/hooks/papi-hooks/use-data.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-data.hook.ts @@ -1,5 +1,5 @@ import { DataProviderSubscriberOptions } from '@shared/models/data-provider.interface'; -import { DataProvider } from '@shared/models/data-provider.model'; +import { IDataProvider } from '@shared/models/data-provider.model'; import useEventAsync from '@renderer/hooks/papi-hooks/use-event-async.hook'; import { useMemo, useState } from 'react'; import { PapiEventAsync, PapiEventHandler } from '@shared/models/papi-event.model'; @@ -44,7 +44,7 @@ function useData( * - `isLoading`: whether the data with the selector is awaiting retrieval from the data provider */ function useData( - dataProvider: DataProvider | undefined, + dataProvider: IDataProvider | undefined, selector: TSelector, defaultValue: TGetData, subscriberOptions?: DataProviderSubscriberOptions, @@ -54,7 +54,7 @@ function useData( * want to consolidate and only get the data provider once) */ function useData( - dataProviderSource: string | DataProvider | undefined, + dataProviderSource: string | IDataProvider | undefined, selector: TSelector, defaultValue: TGetData, subscriberOptions?: DataProviderSubscriberOptions, @@ -64,7 +64,7 @@ function useData( // Get the data provider for this data provider name const dataProvider = - useDataProvider>(dataProviderSource); + useDataProvider>(dataProviderSource); // Indicates if the data with the selector is awaiting retrieval from the data provider const [isLoading, setIsLoading] = useState(true); diff --git a/src/renderer/testing/test-buttons-panel.component.tsx b/src/renderer/testing/test-buttons-panel.component.tsx index df43ef015c..21f937ac83 100644 --- a/src/renderer/testing/test-buttons-panel.component.tsx +++ b/src/renderer/testing/test-buttons-panel.component.tsx @@ -10,7 +10,7 @@ import useEvent from '@renderer/hooks/papi-hooks/use-event.hook'; import { AddWebViewEvent } from '@shared/services/web-view.service'; import useData from '@renderer/hooks/papi-hooks/use-data.hook'; import useDataProvider from '@renderer/hooks/papi-hooks/use-data-provider.hook'; -import { DataProvider } from '@shared/models/data-provider.model'; +import { IDataProvider } from '@shared/models/data-provider.model'; const testBase: (message: string) => Promise = networkService.createRequestFunction('electronAPI.env.test'); @@ -165,7 +165,7 @@ function TestButtonsPanel() { // Test a method on a data provider engine that isn't on the interface to see if you can actually do this const [hasTestedRandomMethod, setHasTestedRandomMethod] = useState(false); const greetingsDataProvider = - useDataProvider>('hello-someone.greetings'); + useDataProvider>('hello-someone.greetings'); if (!hasTestedRandomMethod && greetingsDataProvider) { setHasTestedRandomMethod(true); (async () => { diff --git a/src/shared/models/data-provider.interface.ts b/src/shared/models/data-provider.interface.ts index 7ec97ba37c..26eba72976 100644 --- a/src/shared/models/data-provider.interface.ts +++ b/src/shared/models/data-provider.interface.ts @@ -47,15 +47,19 @@ export type DataProviderSubscriber = ( ) => Promise; /** - * 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. + * 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. + * * @type `TSelector` - the type of selector used to get some data from this provider. * A selector is an object a caller provides to the data provider to tell the provider what subset of data it wants. * Note: A selector must be stringifiable. * @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 + * + * @see IDataProvider */ -interface IDataProvider extends NetworkableObject { +interface DataProviderInternal extends NetworkableObject { /** * Set a subset of data according to the selector. * @@ -88,4 +92,4 @@ interface IDataProvider extends NetworkableObject subscribe: DataProviderSubscriber; } -export default IDataProvider; +export default DataProviderInternal; diff --git a/src/shared/models/data-provider.model.ts b/src/shared/models/data-provider.model.ts index cd85258edb..7448c88943 100644 --- a/src/shared/models/data-provider.model.ts +++ b/src/shared/models/data-provider.model.ts @@ -1,22 +1,31 @@ -import IDataProvider from '@shared/models/data-provider.interface'; +import DataProviderInternal from '@shared/models/data-provider.interface'; import { DisposableNetworkObject, NetworkObject, NetworkableObject } from './network-object.model'; import { CanHaveOnDidDispose } from './disposal.model'; /** - * Information about a data provider. - * Returned from getting a data provider. + * 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. + * @type `TSelector` - the type of selector used to get some data from this provider. + * A selector is an object a caller provides to the data provider to tell the provider what subset of data it wants. + * Note: A selector must be stringifiable. + * @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 */ // Basically a layer over NetworkObject -export interface DataProvider +export interface IDataProvider extends NetworkObject, - CanHaveOnDidDispose> {} + CanHaveOnDidDispose> {} /** - * Information about a data provider including control over disposing of it. - * Returned from registering a data provider (only the process that set it up should dispose of it) + * 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 */ // Basically a layer over DisposableNetworkObject -export interface DisposableDataProvider +export interface IDisposableDataProvider extends DisposableNetworkObject, - // Need to omit dispose here because it is optional on DataProvider but is required on DisposableNetworkObject - Omit, 'dispose'> {} + // Need to omit dispose here because it is optional on IDataProvider but is required on DisposableNetworkObject + Omit, 'dispose'> {} diff --git a/src/shared/services/data-provider.service.ts b/src/shared/services/data-provider.service.ts index 88e255bcd0..c73d2136ec 100644 --- a/src/shared/services/data-provider.service.ts +++ b/src/shared/services/data-provider.service.ts @@ -3,8 +3,8 @@ * Exposed on the papi. */ -import { DataProvider, DisposableDataProvider } from '@shared/models/data-provider.model'; -import IDataProvider, { +import { IDataProvider, IDisposableDataProvider } from '@shared/models/data-provider.model'; +import DataProviderInternal, { DataProviderSubscriber, DataProviderSubscriberOptions, } from '@shared/models/data-provider.interface'; @@ -70,7 +70,7 @@ async function has(providerName: string): Promise { * @returns subscribe function for a data provider */ function createDataProviderSubscriber( - dataProviderContainer: Container>, + dataProviderContainer: Container>, onDidUpdate: PapiEvent, ): DataProviderSubscriber { return async (selector, callback, options?: DataProviderSubscriberOptions) => { @@ -141,14 +141,14 @@ function createDataProviderSubscriber( * WARNING: this function mutates the provided object. Its `notifyUpdate` and `set` methods are layered over to facilitate data provider subscriptions. * @param dataProviderEngine provider engine that handles setting and getting data as well as informing which listeners should get what updates * @param dataProviderContainer container that holds a reference to the network object data provider so the subscribe function can reference the data provider - * @param onDidUpdateEmitter event emitter to use for informing subscribers of updates. The event just returns what set returns (should be true according to IDataProvider) + * @param onDidUpdateEmitter event emitter to use for informing subscribers of updates. The event just returns what set returns (should be true according to IDataProviderEngine) * @returns data provider layering over the provided data provider engine */ function buildDataProvider( dataProviderEngine: IDataProviderEngine, - dataProviderContainer: Container>, + dataProviderContainer: Container>, onDidUpdateEmitter: PapiEventEmitter, -): IDataProvider { +): DataProviderInternal { // Layer over data provider engine methods to give it control over emitting updates // Layer over the data provider engine's notifyUpdate with one that actually emits an update // or if the dpe doesn't have notifyUpdate, give it one @@ -180,7 +180,7 @@ function buildDataProvider( // Currently, set is omitted because it may or may not be provided on the data provider engine, and we want to // throw an exception if someone uses it without it being provided. // TODO: update network objects so remote objects know when methods do not exist, then make IDataProvider.set optional - const dataProviderInternal: Omit, 'set'> = { + const dataProviderInternal: Omit, 'set'> = { /** Layered get that runs the engine's get */ get: dataProviderEngine.get.bind(dataProviderEngine), /** Subscribe to run the callback when data changes. Also immediately calls callback with the current value */ @@ -227,7 +227,7 @@ function buildDataProvider( }, // Type assert the data provider engine proxy because it is a DataProviderInternal although // Typescript can't figure it out - }) as unknown as IDataProvider; + }) as unknown as DataProviderInternal; return dataProvider; } @@ -249,7 +249,7 @@ function buildDataProvider( async function registerEngine( providerName: string, dataProviderEngine: IDataProviderEngine, -): Promise> { +): Promise> { await initialize(); // There is a potential networking sync issue here. We check for a data provider, then we create a network event, then we create a network object. @@ -270,7 +270,7 @@ async function registerEngine( /** Container to hold a reference to the final network object data provider so the local object * can reference the network object in its functions */ - const dataProviderContainer: Container> = { + const dataProviderContainer: Container> = { contents: undefined, }; @@ -290,11 +290,11 @@ async function registerEngine( const disposableDataProvider = (await networkObjectService.set( dataProviderObjectId, dataProviderInternal, - )) as DisposableDataProvider; + )) as IDisposableDataProvider; // Get the local network object proxy for the data provider so you can't call // dataProviderContainer.contents.dispose - const dataProvider = await networkObjectService.get>( + const dataProvider = await networkObjectService.get>( dataProviderObjectId, ); @@ -313,9 +313,9 @@ async function registerEngine( */ // This generic type should be DataProviderInternal because we are making part of a local/internal data provider // eslint-disable-next-line @typescript-eslint/no-explicit-any -function createLocalDataProviderToProxy>( +function createLocalDataProviderToProxy>( dataProviderObjectId: string, - // NetworkObject is our way to convert from DataProviderInternal to IDataProvider without specifying generics + // NetworkObject is our way to convert from DataProviderInternal to IDataProvider without specifying generics dataProviderContainer: Container>, ): Partial { // Create a networked update event @@ -329,18 +329,17 @@ function createLocalDataProviderToProxy>( /** * Get a data provider that has previously been set up - * @param dataProviderName Name of the desired data provider + * @param providerName Name of the desired data provider * @returns The data provider with the given name if one exists, undefined otherwise */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -async function get>( - dataProviderName: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any +async function get>( + providerName: string, ): Promise { await initialize(); // Get the object id for this data provider name - const dataProviderObjectId = getDataProviderObjectId(dataProviderName); + const dataProviderObjectId = getDataProviderObjectId(providerName); // Get the network object for this data provider const dataProvider = (await networkObjectService.get( @@ -349,7 +348,7 @@ async function get>( )) as T; if (!dataProvider) { - logger.info(`No data provider found with name = ${dataProviderName}`); + logger.info(`No data provider found with name = ${providerName}`); return undefined; } From 45888bf5aeaa5303273d5d363744ea41699282f1 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 28 Apr 2023 16:54:16 -0500 Subject: [PATCH 4/4] Changed data provider file names --- .../lib/hello-someone/hello-someone.d.ts | 2 +- extensions/lib/hello-someone/hello-someone.ts | 2 +- extensions/lib/quick-verse/quick-verse.d.ts | 2 +- lib/papi-dts/papi.d.ts | 21 ++-- .../papi-hooks/use-data-provider.hook.ts | 2 +- .../hooks/papi-hooks/use-data.hook.ts | 4 +- .../testing/test-buttons-panel.component.tsx | 2 +- src/shared/models/data-provider.interface.ts | 108 ++++-------------- src/shared/models/data-provider.model.ts | 106 +++++++++++++---- src/shared/services/data-provider.service.ts | 4 +- 10 files changed, 128 insertions(+), 125 deletions(-) diff --git a/extensions/lib/hello-someone/hello-someone.d.ts b/extensions/lib/hello-someone/hello-someone.d.ts index a9a0f6a6d9..9c59fbffda 100644 --- a/extensions/lib/hello-someone/hello-someone.d.ts +++ b/extensions/lib/hello-someone/hello-someone.d.ts @@ -1,4 +1,4 @@ -import type { IDataProvider } from 'shared/models/data-provider.model'; +import type IDataProvider from 'shared/models/data-provider.interface'; export interface GreetingsDataProvider extends IDataProvider { testRandomMethod(things: string): Promise; diff --git a/extensions/lib/hello-someone/hello-someone.ts b/extensions/lib/hello-someone/hello-someone.ts index 1b5dfa5b58..31eaaedf29 100644 --- a/extensions/lib/hello-someone/hello-someone.ts +++ b/extensions/lib/hello-someone/hello-someone.ts @@ -1,7 +1,7 @@ import papi from 'papi'; import type { WebViewContentType } from 'shared/data/web-view.model'; import { UnsubscriberAsync } from 'shared/utils/papi-util'; -import type { IDataProvider } from 'shared/models/data-provider.model'; +import type IDataProvider from 'shared/models/data-provider.interface'; // @ts-expect-error ts(1192) this file has no default export; the text is exported by rollup import helloSomeoneHtmlWebView from './hello-someone.web-view.ejs'; diff --git a/extensions/lib/quick-verse/quick-verse.d.ts b/extensions/lib/quick-verse/quick-verse.d.ts index 3b588ec9cd..5951c247ff 100644 --- a/extensions/lib/quick-verse/quick-verse.d.ts +++ b/extensions/lib/quick-verse/quick-verse.d.ts @@ -1,4 +1,4 @@ -import type { IDataProvider } from 'shared/models/data-provider.model'; +import type IDataProvider from 'shared/models/data-provider.interface'; // TODO: Move these types to quick-verse.ts and generate this file from quick-verse.ts in the future? diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index f55106d36d..3b19e35666 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -1300,7 +1300,7 @@ declare module 'shared/models/network-object.model' { networkObjectContainer: Container>, ) => Partial; } -declare module 'shared/models/data-provider.interface' { +declare module 'shared/models/data-provider.model' { import { UnsubscriberAsync } from 'shared/utils/papi-util'; import { PapiEventHandler } from 'shared/models/papi-event.model'; import { NetworkableObject } from 'shared/models/network-object.model'; @@ -1393,8 +1393,8 @@ declare module 'shared/models/data-provider.interface' { } export default DataProviderInternal; } -declare module 'shared/models/data-provider.model' { - import DataProviderInternal from 'shared/models/data-provider.interface'; +declare module 'shared/models/data-provider.interface' { + import DataProviderInternal from 'shared/models/data-provider.model'; import { DisposableNetworkObject, NetworkObject, @@ -1411,9 +1411,10 @@ declare module 'shared/models/data-provider.model' { * @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 interface IDataProvider + interface IDataProvider extends NetworkObject, CanHaveOnDidDispose> {} + 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) @@ -1474,7 +1475,7 @@ declare module 'shared/services/data-provider.service' { * Handles registering data providers and serving data around the papi. * Exposed on the papi. */ - import { IDataProvider, IDisposableDataProvider } from 'shared/models/data-provider.model'; + import IDataProvider, { IDisposableDataProvider } from 'shared/models/data-provider.interface'; import IDataProviderEngine from 'shared/models/data-provider-engine.model'; /** Determine if a data provider with the given name exists anywhere on the network */ function has(providerName: string): Promise; @@ -1940,7 +1941,7 @@ declare module 'renderer/hooks/papi-hooks/use-event-async.hook' { export default useEventAsync; } declare module 'renderer/hooks/papi-hooks/use-data-provider.hook' { - import { IDataProvider } from 'shared/models/data-provider.model'; + import IDataProvider from 'shared/models/data-provider.interface'; /** * Gets a data provider with specified name * @param providerName name of the data provider to get @@ -1984,8 +1985,8 @@ declare module 'renderer/hooks/papi-hooks/use-data-provider.hook' { export default useDataProvider; } declare module 'renderer/hooks/papi-hooks/use-data.hook' { - import { DataProviderSubscriberOptions } from 'shared/models/data-provider.interface'; - import { IDataProvider } from 'shared/models/data-provider.model'; + import { DataProviderSubscriberOptions } from 'shared/models/data-provider.model'; + import IDataProvider from 'shared/models/data-provider.interface'; /** * Subscribes to run a callback on a data provider's data with specified selector * @param providerName name of the data provider to subscribe to @@ -2137,13 +2138,13 @@ declare module 'papi' { TSetData >, ) => Promise< - import('shared/models/data-provider.model').IDisposableDataProvider< + import('shared/models/data-provider.interface').IDisposableDataProvider< TSelector, TGetData, TSetData > >; - get: >( + get: >( providerName: string, ) => Promise; }; diff --git a/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts b/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts index a9a2b4b6fd..3851b1a915 100644 --- a/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts @@ -1,5 +1,5 @@ import dataProviderService from '@shared/services/data-provider.service'; -import { IDataProvider } from '@shared/models/data-provider.model'; +import IDataProvider from '@shared/models/data-provider.interface'; import { useCallback, useMemo, useState } from 'react'; import useEvent from '@renderer/hooks/papi-hooks/use-event.hook'; import usePromise from '@renderer/hooks/papi-hooks/use-promise.hook'; diff --git a/src/renderer/hooks/papi-hooks/use-data.hook.ts b/src/renderer/hooks/papi-hooks/use-data.hook.ts index b13b84e6a3..a3dd868d26 100644 --- a/src/renderer/hooks/papi-hooks/use-data.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-data.hook.ts @@ -1,5 +1,5 @@ -import { DataProviderSubscriberOptions } from '@shared/models/data-provider.interface'; -import { IDataProvider } from '@shared/models/data-provider.model'; +import { DataProviderSubscriberOptions } from '@shared/models/data-provider.model'; +import IDataProvider from '@shared/models/data-provider.interface'; import useEventAsync from '@renderer/hooks/papi-hooks/use-event-async.hook'; import { useMemo, useState } from 'react'; import { PapiEventAsync, PapiEventHandler } from '@shared/models/papi-event.model'; diff --git a/src/renderer/testing/test-buttons-panel.component.tsx b/src/renderer/testing/test-buttons-panel.component.tsx index 21f937ac83..30e4dcb38d 100644 --- a/src/renderer/testing/test-buttons-panel.component.tsx +++ b/src/renderer/testing/test-buttons-panel.component.tsx @@ -10,7 +10,7 @@ import useEvent from '@renderer/hooks/papi-hooks/use-event.hook'; import { AddWebViewEvent } from '@shared/services/web-view.service'; import useData from '@renderer/hooks/papi-hooks/use-data.hook'; import useDataProvider from '@renderer/hooks/papi-hooks/use-data-provider.hook'; -import { IDataProvider } from '@shared/models/data-provider.model'; +import IDataProvider from '@shared/models/data-provider.interface'; const testBase: (message: string) => Promise = networkService.createRequestFunction('electronAPI.env.test'); diff --git a/src/shared/models/data-provider.interface.ts b/src/shared/models/data-provider.interface.ts index 26eba72976..f58e70f5c8 100644 --- a/src/shared/models/data-provider.interface.ts +++ b/src/shared/models/data-provider.interface.ts @@ -1,95 +1,33 @@ -import { UnsubscriberAsync } from '@shared/utils/papi-util'; -import { PapiEventHandler } from '@shared/models/papi-event.model'; -import { NetworkableObject } from './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. - * - `'all'` - 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' | 'all'; -}; - -/** - * Subscribe to receive updates from this data provider that are relevant to the provided selector. - * - * 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: TSelector, - callback: PapiEventHandler, - options?: DataProviderSubscriberOptions, -) => Promise; +import DataProviderInternal from '@shared/models/data-provider.model'; +import { DisposableNetworkObject, NetworkObject, NetworkableObject } from './network-object.model'; +import { CanHaveOnDidDispose } from './disposal.model'; /** - * 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. - * + * 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. * @type `TSelector` - the type of selector used to get some data from this provider. * A selector is an object a caller provides to the data provider to tell the provider what subset of data it wants. * Note: A selector must be stringifiable. * @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 + */ +// Basically a layer over NetworkObject +interface IDataProvider + extends NetworkObject, + CanHaveOnDidDispose> {} + +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 */ -interface DataProviderInternal extends NetworkableObject { - /** - * 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 true if successfully set (will send updates), false otherwise (will not send updates) - */ - set: (selector: TSelector, data: TSetData) => 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 - */ - get: (selector: TSelector) => Promise; - /** - * Subscribe to receive updates from this data provider that are relevant to the provided selector. - * - * 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 - */ - subscribe: DataProviderSubscriber; -} - -export default DataProviderInternal; +// Basically a layer over DisposableNetworkObject +export interface IDisposableDataProvider + extends DisposableNetworkObject, + // Need to omit dispose here because it is optional on IDataProvider but is required on DisposableNetworkObject + Omit, 'dispose'> {} diff --git a/src/shared/models/data-provider.model.ts b/src/shared/models/data-provider.model.ts index 7448c88943..26eba72976 100644 --- a/src/shared/models/data-provider.model.ts +++ b/src/shared/models/data-provider.model.ts @@ -1,31 +1,95 @@ -import DataProviderInternal from '@shared/models/data-provider.interface'; -import { DisposableNetworkObject, NetworkObject, NetworkableObject } from './network-object.model'; -import { CanHaveOnDidDispose } from './disposal.model'; +import { UnsubscriberAsync } from '@shared/utils/papi-util'; +import { PapiEventHandler } from '@shared/models/papi-event.model'; +import { NetworkableObject } from './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. + * - `'all'` - 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' | 'all'; +}; + +/** + * Subscribe to receive updates from this data provider that are relevant to the provided selector. + * + * 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: TSelector, + callback: PapiEventHandler, + options?: DataProviderSubscriberOptions, +) => Promise; /** - * 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. + * 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. + * * @type `TSelector` - the type of selector used to get some data from this provider. * A selector is an object a caller provides to the data provider to tell the provider what subset of data it wants. * Note: A selector must be stringifiable. * @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 - */ -// Basically a layer over NetworkObject -export interface IDataProvider - extends NetworkObject, - CanHaveOnDidDispose> {} - -/** - * 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 */ -// Basically a layer over DisposableNetworkObject -export interface IDisposableDataProvider - extends DisposableNetworkObject, - // Need to omit dispose here because it is optional on IDataProvider but is required on DisposableNetworkObject - Omit, 'dispose'> {} +interface DataProviderInternal extends NetworkableObject { + /** + * 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 true if successfully set (will send updates), false otherwise (will not send updates) + */ + set: (selector: TSelector, data: TSetData) => 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 + */ + get: (selector: TSelector) => Promise; + /** + * Subscribe to receive updates from this data provider that are relevant to the provided selector. + * + * 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 + */ + subscribe: DataProviderSubscriber; +} + +export default DataProviderInternal; diff --git a/src/shared/services/data-provider.service.ts b/src/shared/services/data-provider.service.ts index c73d2136ec..ed617bb0fb 100644 --- a/src/shared/services/data-provider.service.ts +++ b/src/shared/services/data-provider.service.ts @@ -3,11 +3,11 @@ * Exposed on the papi. */ -import { IDataProvider, IDisposableDataProvider } from '@shared/models/data-provider.model'; +import IDataProvider, { IDisposableDataProvider } from '@shared/models/data-provider.interface'; import DataProviderInternal, { DataProviderSubscriber, DataProviderSubscriberOptions, -} from '@shared/models/data-provider.interface'; +} from '@shared/models/data-provider.model'; import IDataProviderEngine from '@shared/models/data-provider-engine.model'; import { PapiEvent } from '@shared/models/papi-event.model'; import PapiEventEmitter from '@shared/models/papi-event-emitter.model';