Skip to content

Commit

Permalink
Added pendingPromise
Browse files Browse the repository at this point in the history
  • Loading branch information
smikhalevski committed Jul 2, 2024
1 parent bebe4a6 commit 1745dad
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 83 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1321,7 +1321,7 @@ hook:
const accountExecutor = useExecutorManager().getOrCreate('account');
```

You can execute a task in response a user action, for example when user clicks a button:
You can execute a task in response to a user action, for example when user clicks a button:

```tsx
const executor = useExecutor('test');
Expand Down
56 changes: 23 additions & 33 deletions src/main/ExecutorImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@ export class ExecutorImpl<Value = any> implements Executor {
isFulfilled = false;
annotations: ExecutorAnnotations = Object.create(null);
version = 0;

/**
* The promise of the pending task execution, or `null` if there's no pending task execution.
*/
_taskPromise: AbortablePromise<Value> | null = null;
pendingPromise: AbortablePromise<Value> | null = null;

/**
* The number of consumers that activated the executor.
Expand All @@ -46,7 +42,7 @@ export class ExecutorImpl<Value = any> implements Executor {
}

get isPending(): boolean {
return this._taskPromise !== null;
return this.pendingPromise !== null;
}

get isInvalidated(): boolean {
Expand All @@ -65,8 +61,8 @@ export class ExecutorImpl<Value = any> implements Executor {
throw this.isSettled ? this.reason : new Error('The executor is not settled');
}

getOrDefault<DefaultValue>(defaultValue: DefaultValue): Value | DefaultValue {
return this.isFulfilled ? this.value! : defaultValue;
getOrDefault<DefaultValue>(defaultValue?: DefaultValue): Value | DefaultValue | undefined {
return this.isFulfilled ? this.value : defaultValue;
}

getOrAwait(): AbortablePromise<Value> {
Expand Down Expand Up @@ -103,10 +99,10 @@ export class ExecutorImpl<Value = any> implements Executor {
}

execute(task: ExecutorTask<Value>): AbortablePromise<Value> {
const taskPromise = new AbortablePromise<Value>((resolve, reject, signal) => {
const promise = new AbortablePromise<Value>((resolve, reject, signal) => {
signal.addEventListener('abort', () => {
if (this._taskPromise === taskPromise) {
this._taskPromise = null;
if (this.pendingPromise === promise) {
this.pendingPromise = null;
this.version++;
}
this.publish('aborted');
Expand All @@ -120,7 +116,7 @@ export class ExecutorImpl<Value = any> implements Executor {
if (signal.aborted) {
return;
}
this._taskPromise = null;
this.pendingPromise = null;
this.resolve(value);
resolve(value);
},
Expand All @@ -129,30 +125,30 @@ export class ExecutorImpl<Value = any> implements Executor {
if (signal.aborted) {
return;
}
this._taskPromise = null;
this.pendingPromise = null;
this.reject(reason);
reject(reason);
}
);
});

taskPromise.catch(noop);
promise.catch(noop);

const prevTaskPromise = this._taskPromise;
this._taskPromise = taskPromise;
const { pendingPromise } = this;
this.pendingPromise = promise;

if (prevTaskPromise !== null) {
prevTaskPromise.abort(AbortError('The task was replaced'));
if (pendingPromise !== null) {
pendingPromise.abort(AbortError('The task was replaced'));
} else {
this.version++;
}

if (this._taskPromise === taskPromise) {
if (this.pendingPromise === promise) {
this.task = task;
this.publish('pending');
}

return taskPromise;
return promise;
}

retry(): void {
Expand All @@ -172,9 +168,7 @@ export class ExecutorImpl<Value = any> implements Executor {
}

abort(reason: unknown = AbortError('The executor was aborted')): void {
if (this._taskPromise !== null) {
this._taskPromise.abort(reason);
}
this.pendingPromise?.abort(reason);
}

invalidate(invalidatedAt = Date.now()): void {
Expand All @@ -191,12 +185,10 @@ export class ExecutorImpl<Value = any> implements Executor {
return;
}

const taskPromise = this._taskPromise;
this._taskPromise = null;
const { pendingPromise } = this;
this.pendingPromise = null;

if (taskPromise !== null) {
taskPromise.abort();
}
pendingPromise?.abort();

this.isFulfilled = true;
this.value = value;
Expand All @@ -208,12 +200,10 @@ export class ExecutorImpl<Value = any> implements Executor {
}

reject(reason: any, settledAt = Date.now()): void {
const taskPromise = this._taskPromise;
this._taskPromise = null;
const { pendingPromise } = this;
this.pendingPromise = null;

if (taskPromise !== null) {
taskPromise.abort();
}
pendingPromise?.abort();

this.isFulfilled = false;
this.reason = reason;
Expand Down
2 changes: 1 addition & 1 deletion src/main/ssr/SSRExecutorManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class SSRExecutorManager extends ExecutorManager {
const initialVersion = getVersion();

const hasChanges = (): Promise<boolean> =>
Promise.allSettled(Array.from(this._executors.values()).map(executor => executor._taskPromise)).then(() =>
Promise.allSettled(Array.from(this).map(executor => executor.pendingPromise)).then(() =>
Array.from(this).some(executor => executor.isPending) ? hasChanges() : getVersion() !== initialVersion
);

Expand Down
31 changes: 31 additions & 0 deletions src/main/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,23 @@ export interface ExecutorState<Value = any> {
* @template Value The value stored by the executor.
*/
export interface Executor<Value = any> extends ExecutorState<Value>, Observable<ExecutorEvent<Value>> {
/**
* The value of the latest fulfillment.
*
* **Note:** An executor may still have value even if it was {@link isRejected rejected}. Use {@link get},
* {@link getOrDefault}, or {@link getOrAwait} to retrieve a value of the {@link Executor.isFulfilled fulfilled}
* executor.
*/
readonly value: Value | undefined;

/**
* The reason of the latest failure.
*
* **Note:** An executor may still have a rejection reason even if it was {@link Executor.isFulfilled fulfilled}.
* Check {@link isRejected} to ensure that an executor is actually rejected.
*/
readonly reason: any;

/**
* The integer version of {@link ExecutorState the state of this executor} that is incremented every time the executor
* is mutated.
Expand Down Expand Up @@ -219,12 +236,26 @@ export interface Executor<Value = any> extends ExecutorState<Value>, Observable<
*/
readonly task: ExecutorTask<Value> | null;

/**
* The promise of the pending {@link task} execution, or `null` if there's no pending task execution.
*
* **Note:** This promise is aborted if
* [the task is replaced](https://github.com/smikhalevski/react-executor?tab=readme-ov-file#replace-a-task).
* Use {@link getOrAwait} to wait until the executor becomes {@link isSettled settled}.
*/
readonly pendingPromise: AbortablePromise<Value> | null;

/**
* Returns a {@link value} if the executor is {@link isFulfilled fulfilled}. Throws a {@link reason} if the executor
* is {@link isRejected rejected}. Otherwise, throws an {@link !Error}.
*/
get(): Value;

/**
* Returns a {@link value} if the executor is {@link isFulfilled fulfilled}. Otherwise, returns `undefined`.
*/
getOrDefault(): Value | undefined;

/**
* Returns a {@link value} if the executor is {@link isFulfilled fulfilled}. Otherwise, returns the default value.
*
Expand Down
Loading

0 comments on commit 1745dad

Please sign in to comment.