Skip to content

Commit

Permalink
Add the network object status service (#652)
Browse files Browse the repository at this point in the history
  • Loading branch information
lyonsil authored Nov 30, 2023
1 parent 191adb6 commit 031cfb5
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 20 deletions.
31 changes: 31 additions & 0 deletions lib/papi-dts/papi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1709,6 +1709,8 @@ declare module 'shared/services/network-object.service' {
* event contains information about the new network object.
*/
export const onDidCreateNetworkObject: PapiEvent<NetworkObjectDetails>;
/** Event that fires with a network object ID when that object is disposed locally or remotely */
export const onDidDisposeNetworkObject: PapiEvent<string>;
interface IDisposableObject {
dispose?: UnsubscriberAsync;
}
Expand Down Expand Up @@ -2779,6 +2781,35 @@ declare module 'shared/services/web-view.service-model' {
export const EVENT_NAME_ON_DID_ADD_WEB_VIEW: `${string}:${string}`;
export const NETWORK_OBJECT_NAME_WEB_VIEW_SERVICE = 'WebViewService';
}
declare module 'shared/services/network-object-status.service-model' {
import { NetworkObjectDetails } from 'shared/models/network-object.model';
export interface NetworkObjectStatusRemoteServiceType {
/**
* Get details about all available network objects
*
* @returns Object whose keys are the names of the network objects and whose values are the
* {@link NetworkObjectDetails} for each network object
*/
getAllNetworkObjectDetails: () => Promise<Record<string, NetworkObjectDetails>>;
}
/** Provides functions related to the set of available network objects */
export interface NetworkObjectStatusServiceType extends NetworkObjectStatusRemoteServiceType {
/**
* Get a promise that resolves when a network object is registered or rejects if a timeout is hit
*
* @returns Promise that either resolves to the {@link NetworkObjectDetails} for a network object
* once the network object is registered, or rejects if a timeout is provided and the timeout is
* reached before the network object is registered
*/
waitForNetworkObject: (id: string, timeoutInMS?: number) => Promise<NetworkObjectDetails>;
}
export const networkObjectStatusServiceNetworkObjectName = 'NetworkObjectStatusService';
}
declare module 'shared/services/network-object-status.service' {
import { NetworkObjectStatusServiceType } from 'shared/services/network-object-status.service-model';
const networkObjectStatusService: NetworkObjectStatusServiceType;
export default networkObjectStatusService;
}
declare module 'shared/services/web-view.service' {
import { WebViewServiceType } from 'shared/services/web-view.service-model';
const webViewService: WebViewServiceType;
Expand Down
13 changes: 13 additions & 0 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import extensionAssetProtocolService from '@main/services/extension-asset-protoc
import { wait } from '@shared/utils/util';
import { CommandNames } from 'papi-shared-types';
import { SerializedRequestType } from '@shared/utils/papi-util';
import networkObjectStatusService from '@shared/services/network-object-status.service';
import { startNetworkObjectStatusService } from './services/network-object-status.service-host';
// Used with the commented out code at the bottom of this file to test the ParatextProjectDataProvider
// import { get } from '@shared/services/project-data-provider.service';
// import { VerseRef } from '@sillsdev/scripture';
Expand Down Expand Up @@ -63,6 +65,9 @@ async function main() {
// The network service relies on nothing else, and other things rely on it, so start it first
await networkService.initialize();

// The network object status service relies on seeing everything else start up later
await startNetworkObjectStatusService();

// The .NET data provider relies on the network service and nothing else
dotnetDataProvider.start();

Expand Down Expand Up @@ -313,6 +318,14 @@ async function main() {
} else logger.error('Could not get testExtensionHost from main');
}, 5000);

setTimeout(async () => {
logger.info(
`Available network objects after 30 seconds: ${JSON.stringify(
await networkObjectStatusService.getAllNetworkObjectDetails(),
)}`,
);
}, 30000);

// #endregion

// #region Test a .NET data provider
Expand Down
51 changes: 51 additions & 0 deletions src/main/services/network-object-status.service-host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { NetworkObjectDetails } from '@shared/models/network-object.model';
import logger from '@shared/services/logger.service';
import {
NetworkObjectStatusRemoteServiceType,
networkObjectStatusServiceNetworkObjectName,
} from '@shared/services/network-object-status.service-model';
import networkObjectService, {
onDidCreateNetworkObject,
onDidDisposeNetworkObject,
} from '@shared/services/network-object.service';

// We are assuming these events get hooked up before any network objects get registered. That allows
// us to start from a clean map. If somehow network objects can be registered before we hook up
// the events, then we have to figure out a way to insert pre-existing objects into the map in a way
// that avoids race conditions with the events that fire around the same time.
const networkObjectIDsToDetails = new Map<string, NetworkObjectDetails>();

onDidCreateNetworkObject((networkObjectDetails) => {
if (networkObjectIDsToDetails.has(networkObjectDetails.id))
logger.warn(`Re-saving network object details for ${networkObjectDetails.id}`);
networkObjectIDsToDetails.set(networkObjectDetails.id, networkObjectDetails);
});

onDidDisposeNetworkObject((networkObjectId) => {
if (!networkObjectIDsToDetails.delete(networkObjectId))
logger.warn(`Notification of disposed object ${networkObjectId} that was previously unknown`);
});

// Making this async to align with the service model even though it could really be synchronous
async function getAllNetworkObjectDetails(): Promise<Record<string, NetworkObjectDetails>> {
const allNetworkObjectDetails: Record<string, NetworkObjectDetails> = {};
networkObjectIDsToDetails.forEach((value: NetworkObjectDetails, key: string) => {
allNetworkObjectDetails[key] = value;
});
return Promise.resolve(allNetworkObjectDetails);
}

const networkObjectStatusService: NetworkObjectStatusRemoteServiceType = {
getAllNetworkObjectDetails,
};

/** Register the network object that backs the network object status service */
// This doesn't really represent this service module, so we're not making it default. To use this
// service, you should use `network-object-status.service.ts`
// eslint-disable-next-line import/prefer-default-export
export async function startNetworkObjectStatusService(): Promise<void> {
await networkObjectService.set<NetworkObjectStatusRemoteServiceType>(
networkObjectStatusServiceNetworkObjectName,
networkObjectStatusService,
);
}
27 changes: 27 additions & 0 deletions src/shared/services/network-object-status.service-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NetworkObjectDetails } from '@shared/models/network-object.model';

// Functions that are exposed through the network object
export interface NetworkObjectStatusRemoteServiceType {
/**
* Get details about all available network objects
*
* @returns Object whose keys are the names of the network objects and whose values are the
* {@link NetworkObjectDetails} for each network object
*/
getAllNetworkObjectDetails: () => Promise<Record<string, NetworkObjectDetails>>;
}

// Functions that are added in the service client on top of what is provided by the network object
/** Provides functions related to the set of available network objects */
export interface NetworkObjectStatusServiceType extends NetworkObjectStatusRemoteServiceType {
/**
* Get a promise that resolves when a network object is registered or rejects if a timeout is hit
*
* @returns Promise that either resolves to the {@link NetworkObjectDetails} for a network object
* once the network object is registered, or rejects if a timeout is provided and the timeout is
* reached before the network object is registered
*/
waitForNetworkObject: (id: string, timeoutInMS?: number) => Promise<NetworkObjectDetails>;
}

export const networkObjectStatusServiceNetworkObjectName = 'NetworkObjectStatusService';
77 changes: 77 additions & 0 deletions src/shared/services/network-object-status.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { NetworkObjectDetails } from '@shared/models/network-object.model';
import {
networkObjectStatusServiceNetworkObjectName,
NetworkObjectStatusRemoteServiceType,
NetworkObjectStatusServiceType,
} from '@shared/services/network-object-status.service-model';
import networkObjectService, {
onDidCreateNetworkObject,
} from '@shared/services/network-object.service';
import AsyncVariable from '@shared/utils/async-variable';

let networkObject: NetworkObjectStatusRemoteServiceType;
let initializationPromise: Promise<void>;
async function initialize(): Promise<void> {
if (!initializationPromise) {
initializationPromise = new Promise<void>((resolve, reject) => {
const executor = async () => {
try {
const localNetworkObjectStatusService =
await networkObjectService.get<NetworkObjectStatusServiceType>(
networkObjectStatusServiceNetworkObjectName,
);
if (!localNetworkObjectStatusService)
throw new Error(
`${networkObjectStatusServiceNetworkObjectName} is not available as a network object`,
);
networkObject = localNetworkObjectStatusService;
resolve();
} catch (error) {
reject(error);
}
};
executor();
});
}
return initializationPromise;
}

// If we ever want to be more clever, we could just keep a local (to this process) cache of the
// active network objects. If we do that, we'll have to deal with initial race conditions around
// getting a network object disposed message in this process before handling the snapshot from
// the service host that includes the (now disposed) network object. Just asking the remote service
// is less error prone, but slower, whenever we get a request for the latest network objects.
async function getAllNetworkObjectDetails(): Promise<Record<string, NetworkObjectDetails>> {
await initialize();
return networkObject.getAllNetworkObjectDetails();
}

// Ideally we would use this inside the network object service to be event-based instead of polling
// while waiting for network objects to be created. That would create a circular dependency between
// this service and the network object service, though, which is most easily resolved by merging
// this code into the network object service. That service is pretty big as it is, so to optimize
// for code understandability we'll just leave it as-is and poll inside the network object service
// `get` for now. Other services will have to call this directly if they want to be event based.
async function waitForNetworkObject(
id: string,
timeoutInMS?: number,
): Promise<NetworkObjectDetails> {
const asyncVar = new AsyncVariable<NetworkObjectDetails>(`waiting-for-${id}`, timeoutInMS ?? -1);
// Watch the stream of incoming network objects before getting a snapshot to avoid race conditions
const unsub = onDidCreateNetworkObject((networkObjectDetails) => {
if (networkObjectDetails.id === id) asyncVar.resolveToValue(networkObjectDetails, false);
if (asyncVar.hasSettled) unsub();
});
// Now check if the needed network object has already been created
const existingNetworkObjectDetails = await getAllNetworkObjectDetails();
if (existingNetworkObjectDetails[id])
asyncVar.resolveToValue(existingNetworkObjectDetails[id], false);
return asyncVar.promise;
}

const networkObjectStatusService: NetworkObjectStatusServiceType = {
getAllNetworkObjectDetails,
waitForNetworkObject,
};

export default networkObjectStatusService;
2 changes: 1 addition & 1 deletion src/shared/services/network-object.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ const onDidDisposeNetworkObjectEmitter = networkService.createNetworkEventEmitte
);

/** Event that fires with a network object ID when that object is disposed locally or remotely */
const onDidDisposeNetworkObject = onDidDisposeNetworkObjectEmitter.event;
export const onDidDisposeNetworkObject = onDidDisposeNetworkObjectEmitter.event;

/** Runs to dispose of local and remote network objects when we receive events telling us to do so */
onDidDisposeNetworkObject((id: string) => {
Expand Down
28 changes: 9 additions & 19 deletions src/shared/services/web-view.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import {
} from '@shared/services/web-view.service-model';
import { AddWebViewEvent, Layout } from '@shared/models/docking-framework.model';
import networkObjectService from '@shared/services/network-object.service';
import { wait } from '@shared/utils/util';
import logger from '@shared/services/logger.service';
import networkObjectStatusService from './network-object-status.service';

const onDidAddWebView: PapiEvent<AddWebViewEvent> = getNetworkEvent<AddWebViewEvent>(
EVENT_NAME_ON_DID_ADD_WEB_VIEW,
Expand All @@ -22,24 +21,15 @@ async function initialize(): Promise<void> {
initializationPromise = new Promise<void>((resolve, reject) => {
const executor = async () => {
try {
// Normally automatic retrying within the network object service is sufficient
// However, in this case we know webViewService is being requested before it is registered
// Give it some extra time to be registered by the renderer
let localWebViewService: WebViewServiceType | undefined;
const maxAttempts: number = 3;
for (let attemptsRemaining = maxAttempts; attemptsRemaining > 0; attemptsRemaining--) {
// eslint-disable-next-line no-await-in-loop
localWebViewService = await networkObjectService.get<WebViewServiceType>(
NETWORK_OBJECT_NAME_WEB_VIEW_SERVICE,
);
if (localWebViewService) break;

// eslint-disable-next-line no-await-in-loop
await wait(1000);

logger.debug(`Retrying to get the web view service`);
}
await networkObjectStatusService.waitForNetworkObject(
NETWORK_OBJECT_NAME_WEB_VIEW_SERVICE,
// Wait 30 seconds for the web view service to appear
30000,
);

const localWebViewService = await networkObjectService.get<WebViewServiceType>(
NETWORK_OBJECT_NAME_WEB_VIEW_SERVICE,
);
if (!localWebViewService)
throw new Error(
`${NETWORK_OBJECT_NAME_WEB_VIEW_SERVICE} is not available as a network object`,
Expand Down

0 comments on commit 031cfb5

Please sign in to comment.