Skip to content

Commit

Permalink
Wait for an executor
Browse files Browse the repository at this point in the history
  • Loading branch information
smikhalevski committed Apr 27, 2024
1 parent e94a946 commit e82c99b
Show file tree
Hide file tree
Showing 12 changed files with 82 additions and 83 deletions.
11 changes: 5 additions & 6 deletions src/main/ExecutorImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,8 @@ export class ExecutorImpl<Value = any> implements Executor {
return this.isFulfilled ? this.value! : defaultValue;
}

then<Result1 = Value, Result2 = never>(
onFulfilled?: ((value: Value) => PromiseLike<Result1> | Result1) | null,
onRejected?: ((reason: any) => PromiseLike<Result2> | Result2) | null
): Promise<Result1 | Result2> {
return new Promise<Value>((resolve, reject) => {
toPromise(): AbortablePromise<Value> {
return new AbortablePromise((resolve, reject, signal) => {
if (this.isSettled && !this.isPending) {
resolve(this.get());
return;
Expand All @@ -88,7 +85,9 @@ export class ExecutorImpl<Value = any> implements Executor {
}
}
});
}).then(onFulfilled, onRejected);

signal.addEventListener('abort', unsubscribe);
});
}

execute(task: ExecutorTask<Value>): AbortablePromise<Value> {
Expand Down
27 changes: 26 additions & 1 deletion src/main/ExecutorManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PubSub } from 'parallel-universe';
import { AbortablePromise, PubSub } from 'parallel-universe';
import { ExecutorImpl } from './ExecutorImpl';
import type { Executor, ExecutorEvent, ExecutorPlugin, ExecutorTask } from './types';

Expand Down Expand Up @@ -70,6 +70,31 @@ export class ExecutorManager implements Iterable<Executor> {
return executor;
}

/**
* Resolves with the existing executor or waits for an executor to be created.
*
* @param key The executor key to wait for.
*/
waitFor(key: string): AbortablePromise<Executor> {
return new AbortablePromise((resolve, _reject, signal) => {
const executor = this._executors.get(key);

if (executor !== undefined) {
resolve(executor);
return;
}

const unsubscribe = this.subscribe(event => {
if (event.target.key === key && event.type === 'configured') {
unsubscribe();
resolve(event.target);
}
});

signal.addEventListener('abort', unsubscribe);
});
}

/**
* Deletes the non-{@link Executor.isActive active} executor from the manager.
*
Expand Down
15 changes: 2 additions & 13 deletions src/main/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export type ExecutorTask<Value = any> = (signal: AbortSignal, executor: Executor
*
* @template Value The value stored by the executor.
*/
export interface Executor<Value = any> extends PromiseLike<Value> {
export interface Executor<Value = any> {
/**
* The key of this executor, unique in scope of the {@link manager}.
*/
Expand Down Expand Up @@ -210,22 +210,11 @@ export interface Executor<Value = any> extends PromiseLike<Value> {
getOrDefault(defaultValue: Value): Value;

/**
* Attaches callbacks for the fulfillment and/or rejection of the executor.
*
* For a non-{@link isPending pending} and {@link isSettled settled} executor, the promise is resolved with the
* available {@link value}, or rejected with the available {@link reason}. Otherwise, the promise waits for the
* executor to become settled and then settles as well.
*
* @param onFulfilled The callback to execute when the executor is fulfilled.
* @param onRejected The callback to execute when the executor is rejected.
* @returns A promise for the completion of whichever callback is executed.
* @template Result1 The result of the fulfillment callback.
* @template Result2 The result of the rejection callback.
*/
then<Result1 = Value, Result2 = never>(
onFulfilled?: ((value: Value) => PromiseLike<Result1> | Result1) | null,
onRejected?: ((reason: any) => PromiseLike<Result2> | Result2) | null
): Promise<Result1 | Result2>;
toPromise(): AbortablePromise<Value>;

/**
* Executes a task and populates the executor with the returned result.
Expand Down
9 changes: 4 additions & 5 deletions src/main/useExecutorSuspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export function useExecutorSuspense(executors: Executor | Executor[]) {
const promises = executors.reduce(reducePending, null);

if (promises !== null) {
throw Promise.allSettled(promises).then(noop, noop);
throw Promise.all(promises).then(noop, noop);
}
} else if (executors.isPending) {
throw executors.then(noop, noop);
throw executors.toPromise().then(noop, noop);
}

return executors;
Expand All @@ -36,10 +36,9 @@ export function useExecutorSuspense(executors: Executor | Executor[]) {
function reducePending(promises: PromiseLike<unknown>[] | null, executor: Executor): PromiseLike<unknown>[] | null {
if (executor.isPending) {
if (promises === null) {
promises = [executor];
} else {
promises.push(executor);
promises = [];
}
promises.push(executor.toPromise().then(noop, noop));
}
return promises;
}
38 changes: 6 additions & 32 deletions src/test/ExecutorImpl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,55 +616,29 @@ describe('ExecutorImpl', () => {
});
});

describe('then', () => {
describe('toPromise', () => {
test('resolves with the value if an executor is fulfilled', async () => {
executor.resolve('aaa');

await expect(executor.then()).resolves.toBe('aaa');
await expect(executor.toPromise()).resolves.toBe('aaa');
});

test('rejects with the reason if an executor is fulfilled', async () => {
executor.reject(expectedReason);

await expect(executor.then()).rejects.toBe(expectedReason);
});

test('calls onFulfilled callback', async () => {
const onFulfilledMock = jest.fn(_value => 'bbb');
const onRejectedMock = jest.fn();

executor.resolve('aaa');

await expect(executor.then(onFulfilledMock, onRejectedMock)).resolves.toBe('bbb');

expect(onFulfilledMock).toHaveBeenCalledTimes(1);
expect(onFulfilledMock.mock.calls[0][0]).toBe('aaa');
expect(onRejectedMock).toHaveBeenCalledTimes(0);
});

test('calls onRejected callback', async () => {
const onFulfilledMock = jest.fn();
const onRejectedMock = jest.fn(_reason => 'aaa');

executor.reject(expectedReason);

await expect(executor.then(onFulfilledMock, onRejectedMock)).resolves.toBe('aaa');

expect(onFulfilledMock).toHaveBeenCalledTimes(0);
expect(onRejectedMock).toHaveBeenCalledTimes(1);
expect(onRejectedMock.mock.calls[0][0]).toBe(expectedReason);
await expect(executor.toPromise()).rejects.toBe(expectedReason);
});

test('waits for the executor to be fulfilled', async () => {
const promise = executor.then();
const promise = executor.toPromise();

executor.resolve('aaa');

await expect(promise).resolves.toBe('aaa');
});

test('waits for the executor to be rejected', async () => {
const promise = executor.then();
const promise = executor.toPromise();

executor.reject(expectedReason);

Expand All @@ -675,7 +649,7 @@ describe('ExecutorImpl', () => {
executor.resolve('aaa');
executor.execute(() => 'bbb').catch(noop);

const promise = executor.then();
const promise = executor.toPromise();

executor.execute(() => 'ccc');

Expand Down
17 changes: 15 additions & 2 deletions src/test/ExecutorManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('ExecutorManager', () => {
expect(listenerMock).toHaveBeenNthCalledWith(1, { type: 'configured', target: executor });
expect(listenerMock).toHaveBeenNthCalledWith(2, { type: 'pending', target: executor });

await expect(executor.then()).resolves.toBe(111);
await expect(executor.toPromise()).resolves.toBe(111);

expect(executor.isPending).toBe(false);
expect(executor.isFulfilled).toBe(true);
Expand Down Expand Up @@ -136,7 +136,7 @@ describe('ExecutorManager', () => {
expect(listenerMock).toHaveBeenCalledTimes(1);
expect(listenerMock).toHaveBeenNthCalledWith(1, { type: 'configured', target: executor });

await executor;
await executor.toPromise();

expect(executor.value).toBe(222);

Expand Down Expand Up @@ -172,6 +172,19 @@ describe('ExecutorManager', () => {
});
});

describe('waitFor', () => {
test('waits for an executor', async () => {
await expect(manager.waitFor('aaa')).resolves.toBe(manager.getOrCreate('aaa'));
});

test('resolves with the existing executor', async () => {
const executor = manager.getOrCreate('aaa');
const executorPromise = manager.waitFor('aaa');

await expect(executorPromise).resolves.toBe(executor);
});
});

describe('dispose', () => {
test('no-op is there is no executor with the key', () => {
expect(manager.dispose('aaa')).toBe(false);
Expand Down
4 changes: 2 additions & 2 deletions src/test/plugin/retryFocused.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('retryFocused', () => {

executor.activate();

await executor;
await executor.toPromise();

expect(executor.value).toBe('aaa');

Expand All @@ -24,7 +24,7 @@ describe('retryFocused', () => {
expect(executor.isPending).toBe(true);
expect(executor.value).toBe('aaa');

await executor;
await executor.toPromise();

expect(executor.isPending).toBe(false);
expect(executor.value).toBe('bbb');
Expand Down
14 changes: 7 additions & 7 deletions src/test/plugin/retryFulfilled.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ describe('retryFulfilled', () => {

executor.activate();
expect(executor.isPending).toBe(true);
await executor;
await executor.toPromise();
expect(executor.isPending).toBe(false);

// Retry 1
jest.runAllTimers();
expect(executor.isPending).toBe(true);
await executor;
await executor.toPromise();
expect(executor.isPending).toBe(false);

// Retry 2
jest.runAllTimers();
expect(executor.isPending).toBe(true);
await executor;
await executor.toPromise();
expect(executor.isPending).toBe(false);

// Retry 3
Expand All @@ -50,7 +50,7 @@ describe('retryFulfilled', () => {

executor.activate();
expect(executor.isPending).toBe(true);
await executor;
await executor.toPromise();
expect(executor.isPending).toBe(false);

// Retry 1
Expand All @@ -73,7 +73,7 @@ describe('retryFulfilled', () => {

executor.activate();
expect(executor.isPending).toBe(true);
await executor;
await executor.toPromise();
expect(executor.isPending).toBe(false);

// Retry 1
Expand All @@ -96,15 +96,15 @@ describe('retryFulfilled', () => {

const deactivate = executor.activate();
expect(executor.isPending).toBe(true);
await executor;
await executor.toPromise();
expect(executor.isPending).toBe(false);

// Retry 1
jest.runAllTimers();
expect(executor.isPending).toBe(true);

deactivate();
await executor;
await executor.toPromise();

// Retry 2
jest.runAllTimers();
Expand Down
14 changes: 7 additions & 7 deletions src/test/plugin/retryRejected.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,21 @@ describe('retryRejected', () => {
executor.activate();
expect(executor.isPending).toBe(true);
(executor as ExecutorImpl)._promise!.catch(noop);
await executor.then(noop, noop);
await executor.toPromise().then(noop, noop);
expect(executor.isPending).toBe(false);

// Retry 1
jest.runAllTimers();
expect(executor.isPending).toBe(true);
(executor as ExecutorImpl)._promise!.catch(noop);
await executor.then(noop, noop);
await executor.toPromise().then(noop, noop);
expect(executor.isPending).toBe(false);

// Retry 2
jest.runAllTimers();
expect(executor.isPending).toBe(true);
(executor as ExecutorImpl)._promise!.catch(noop);
await executor.then(noop, noop);
await executor.toPromise().then(noop, noop);
expect(executor.isPending).toBe(false);

// Retry 3
Expand All @@ -62,7 +62,7 @@ describe('retryRejected', () => {
executor.activate();
expect(executor.isPending).toBe(true);
(executor as ExecutorImpl)._promise!.catch(noop);
await executor.then(noop, noop);
await executor.toPromise().then(noop, noop);
expect(executor.isPending).toBe(false);

// Retry 1
Expand All @@ -89,7 +89,7 @@ describe('retryRejected', () => {
executor.activate();
expect(executor.isPending).toBe(true);
(executor as ExecutorImpl)._promise!.catch(noop);
await executor.then(noop, noop);
await executor.toPromise().then(noop, noop);
expect(executor.isPending).toBe(false);

// Retry 1
Expand All @@ -116,7 +116,7 @@ describe('retryRejected', () => {
const deactivate = executor.activate();
expect(executor.isPending).toBe(true);
(executor as ExecutorImpl)._promise!.catch(noop);
await executor.then(noop, noop);
await executor.toPromise().then(noop, noop);
expect(executor.isPending).toBe(false);

// Retry 1
Expand All @@ -126,7 +126,7 @@ describe('retryRejected', () => {
deactivate();

(executor as ExecutorImpl)._promise!.catch(noop);
await executor.then(noop, noop);
await executor.toPromise().then(noop, noop);

// Retry 2
jest.runAllTimers();
Expand Down
Loading

0 comments on commit e82c99b

Please sign in to comment.