Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed views not reopening on restart by fixing PlatformEventEmitter bug #947

Merged
merged 1 commit into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/papi-dts/papi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3042,6 +3042,10 @@ declare module 'shared/models/network-object-status.service-model' {
/**
* Get a promise that resolves when a network object is registered or rejects if a timeout is hit
*
* @param objectDetailsToMatch Subset of object details on the network object to wait for.
* Compared to object details using {@link isSubset}
* @param timeoutInMS Max duration to wait for the network object. If not provided, it will wait
* indefinitely
* @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
Expand Down
2 changes: 1 addition & 1 deletion lib/platform-bible-utils/dist/index.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/platform-bible-utils/dist/index.cjs.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions lib/platform-bible-utils/dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export declare class AsyncVariable<T> {
* @param variableName Name to use when logging about this variable
* @param rejectIfNotSettledWithinMS Milliseconds to wait before verifying if the promise was
* settled (resolved or rejected); will reject if it has not settled by that time. Use -1 if you
* do not want a timeout at all.
* do not want a timeout at all. Defaults to 10000 ms
*/
constructor(variableName: string, rejectIfNotSettledWithinMS?: number);
/**
Expand All @@ -35,15 +35,15 @@ export declare class AsyncVariable<T> {
*
* @param value This variable's promise will resolve to this value
* @param throwIfAlreadySettled Determines whether to throw if the variable was already resolved
* or rejected
* or rejected. Defaults to `false`
*/
resolveToValue(value: T, throwIfAlreadySettled?: boolean): void;
/**
* Reject this variable's promise for the value with the given reason
*
* @param reason This variable's promise will be rejected with this reason
* @param throwIfAlreadySettled Determines whether to throw if the variable was already resolved
* or rejected
* or rejected. Defaults to `false`
*/
rejectWithReason(reason: string, throwIfAlreadySettled?: boolean): void;
/** Prevent any further updates to this variable */
Expand Down
57 changes: 28 additions & 29 deletions lib/platform-bible-utils/dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/platform-bible-utils/dist/index.js.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions lib/platform-bible-utils/src/async-variable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default class AsyncVariable<T> {
* @param variableName Name to use when logging about this variable
* @param rejectIfNotSettledWithinMS Milliseconds to wait before verifying if the promise was
* settled (resolved or rejected); will reject if it has not settled by that time. Use -1 if you
* do not want a timeout at all.
* do not want a timeout at all. Defaults to 10000 ms
*/
constructor(variableName: string, rejectIfNotSettledWithinMS: number = 10000) {
this.variableName = variableName;
Expand Down Expand Up @@ -54,7 +54,7 @@ export default class AsyncVariable<T> {
*
* @param value This variable's promise will resolve to this value
* @param throwIfAlreadySettled Determines whether to throw if the variable was already resolved
* or rejected
* or rejected. Defaults to `false`
*/
resolveToValue(value: T, throwIfAlreadySettled: boolean = false): void {
if (this.resolver) {
Expand All @@ -72,7 +72,7 @@ export default class AsyncVariable<T> {
*
* @param reason This variable's promise will be rejected with this reason
* @param throwIfAlreadySettled Determines whether to throw if the variable was already resolved
* or rejected
* or rejected. Defaults to `false`
*/
rejectWithReason(reason: string, throwIfAlreadySettled: boolean = false): void {
if (this.rejecter) {
Expand Down
56 changes: 56 additions & 0 deletions lib/platform-bible-utils/src/platform-event-emitter.model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import PlatformEventEmitter from './platform-event-emitter.model';

describe('unsubscribing', () => {
it('does not prevent other subscribers from running when unsubscribing in a callback', () => {
const shouldUnsubscribeSubscriptions = [false, true, false, true, true, false];
// Map of event number to which event subscription indices ran for that event number
// Purposely making this an array, not a Set, to make sure we catch duplicate runs
const subscriptionResults: { [eventNum: string]: number[] } = {};

const numEventsToEmit = 3;
// Array of each event number that should have been run: [0, 1, 2, ..., numEventsToEdit - 1]
const eventNumArray = [...Array(numEventsToEmit).keys()];
// Array of each event number that should have been run after unsubscribing
// (basically eventNumArray without the first event)
const [, ...eventNumArrayAfterUnsubscribing] = eventNumArray;
let nextEventNum = 0;
const emitter = new PlatformEventEmitter<number>();
const emitEvent = () => {
emitter.emit(nextEventNum);
nextEventNum += 1;
};
const unsubscribers = shouldUnsubscribeSubscriptions.map((shouldUnsubscribe, i) =>
emitter.subscribe((eventNum) => {
const subscriptionResultsForEventNum = subscriptionResults[eventNum] ?? [];
if (!subscriptionResults[eventNum])
subscriptionResults[eventNum] = subscriptionResultsForEventNum;

subscriptionResultsForEventNum.push(i);

if (shouldUnsubscribe) unsubscribers[i]();
}),
);

for (let i = 0; i < numEventsToEmit; i += 1) emitEvent();

// There should be results for each event that was run
expect(
Object.keys(subscriptionResults)
.map((eventNumString) => parseInt(eventNumString, 10))
.sort(),
).toEqual(eventNumArray);

// All should have run the first time
expect(subscriptionResults[0]).toEqual(
shouldUnsubscribeSubscriptions.map((_shouldUnsubscribe, i) => i),
);
eventNumArrayAfterUnsubscribing.forEach((eventNum) => {
// Only the `false` ones (didn't unsubscribe) should have run after the first time
expect(subscriptionResults[eventNum]).toEqual(
shouldUnsubscribeSubscriptions
.map((shouldUnsubscribe, i) => (shouldUnsubscribe ? undefined : i))
.filter((i) => i !== undefined),
);
});
});
});
5 changes: 4 additions & 1 deletion lib/platform-bible-utils/src/platform-event-emitter.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ export default class PlatformEventEmitter<T> implements Dispose {
protected emitFn(event: T) {
this.assertNotDisposed();

this.subscriptions?.forEach((callback) => callback(event));
// Clone the subscriptions array before iterating over the callbacks so the callback index
// doesn't get messed up if someone subscribes or unsubscribes inside one of the callbacks
const emitCallbacks = [...(this.subscriptions ?? [])];
emitCallbacks.forEach((callback) => callback(event));
}

/** Check to make sure this emitter is not disposed. Throw if it is */
Expand Down
7 changes: 7 additions & 0 deletions src/shared/models/network-object-status.service-model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { NetworkObjectDetails } from '@shared/models/network-object.model';
// Linked in TSDoc
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { isSubset } from 'platform-bible-utils';

// Functions that are exposed through the network object
export interface NetworkObjectStatusRemoteServiceType {
Expand All @@ -17,6 +20,10 @@ export interface NetworkObjectStatusServiceType extends NetworkObjectStatusRemot
/**
* Get a promise that resolves when a network object is registered or rejects if a timeout is hit
*
* @param objectDetailsToMatch Subset of object details on the network object to wait for.
* Compared to object details using {@link isSubset}
* @param timeoutInMS Max duration to wait for the network object. If not provided, it will wait
* indefinitely
* @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
Expand Down
Loading
Loading