Skip to content

Commit

Permalink
feat(action): added indirect cancellation to action extension
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewcourtice committed Nov 1, 2021
1 parent c7f3844 commit a16f083
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 15 deletions.
22 changes: 18 additions & 4 deletions docs/src/extensibility/extensions/action.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ npm install @harlem/extension-action

To get started simply register this extension with the store you wish to extend.

```typescript{16-26,29}
```typescript{16-27,30}
import actionExtension from '@harlem/extension-action';
import {
Expand All @@ -61,6 +61,7 @@ const {
getActionErrors,
whenActionIdle,
resetActionState,
abortAction,
onBeforeAction,
onAfterAction,
onActionSuccess,
Expand Down Expand Up @@ -134,7 +135,12 @@ async function runAction() {


### Cancelling an action
Each time an action is called it returns an instance of a `Task` class. The `Task` class is an extension of the in-built `Promise` class that adds an `abort` method you can use to terminate the action.
There are 2 ways to cancel a running action:

- Direct
- Indirect

The **direct** method is simply calling the action and then calling the `abort` method. Each time an action is called it returns an instance of a `Task` class. The `Task` class is an extension of the in-built `Promise` class that adds an `abort` method you can use to terminate the action.

```typescript
async function runAction() {
Expand All @@ -146,7 +152,15 @@ async function runAction() {
}
```

Cancelling the task will throw an `ActionAbortError`. It is recommended to wrap actions you intend on cancelling in a `try/catch` statement to handle this.
The **indirect** method is using the helper method on the store to cancel the action by name.

```typescript
abortAction('load-user-data');
```

::: warning
Cancelling the task will throw an `ActionAbortError` where the action is executed. It is recommended to wrap actions you intend on cancelling (or that are not parallel) in a `try/catch` statement to handle this.
:::


### Handling nested actions
Expand All @@ -167,7 +181,7 @@ export default action('parent-action', async (id: number, mutate, controller) =>
```

### Checking action status
This extension provides a set of helper methods for checking the status of actions. There are 2 ways to check whether an action is running:
This extension provides a set of helper methods for checking the status of actions. Similar to cancelling an action, there are 2 ways to check whether an action is running:

- Direct
- Indirect
Expand Down
16 changes: 14 additions & 2 deletions extensions/action/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const {
getActionErrors,
whenActionIdle,
resetActionState,
abortAction,
onBeforeAction,
onAfterAction,
onActionSuccess,
Expand Down Expand Up @@ -123,7 +124,12 @@ async function runAction() {


### Cancelling an action
Each time an action is called it returns an instance of a `Task` class. The `Task` class is an extension of the in-built `Promise` class that adds an `abort` method you can use to terminate the action.
There are 2 ways to cancel a running action:

- Direct
- Indirect

The **direct** method is simply calling the action and then calling the `abort` method. Each time an action is called it returns an instance of a `Task` class. The `Task` class is an extension of the in-built `Promise` class that adds an `abort` method you can use to terminate the action.

```typescript
async function runAction() {
Expand All @@ -135,6 +141,12 @@ async function runAction() {
}
```

The **indirect** method is using the helper method on the store to cancel the action by name.

```typescript
abortAction('load-user-data');
```

Cancelling the task will throw an `ActionAbortError`. It is recommended to wrap actions you intend on cancelling in a `try/catch` statement to handle this.


Expand All @@ -156,7 +168,7 @@ export default action('parent-action', async (id: number, mutate, controller) =>
```

### Checking action status
This extension provides a set of helper methods for checking the status of actions. There are 2 ways to check whether an action is running:
This extension provides a set of helper methods for checking the status of actions. Similar to cancelling an action, there are 2 ways to check whether an action is running:

- Direct
- Indirect
Expand Down
39 changes: 31 additions & 8 deletions extensions/action/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export default function actionsExtension<TState extends BaseState>() {
return (store: InternalStore<TState>) => {
const _store = store as unknown as InternalStore<TState & ActionStoreState>;

const actionTasks = new Map<string, Set<Task<unknown>>>();

_store.write('$action-init', SENDER, state => state[STATE_PROP] = {}, true);

function setActionState(state: TState & ActionStoreState, name: string) {
Expand All @@ -54,6 +56,14 @@ export default function actionsExtension<TState extends BaseState>() {
function registerAction(name: string) {
_store.register('actions', name, () => () => {});
_store.write('$action-register', SENDER, state => setActionState(state, name), true);

const tasks = new Set<Task<unknown>>();

actionTasks.set(name, tasks);

return {
tasks,
};
}

function incrementRunCount(name: string) {
Expand All @@ -77,9 +87,9 @@ export default function actionsExtension<TState extends BaseState>() {
}

function action<TPayload, TResult = void>(name: string, body: ActionBody<TState, TPayload, TResult>, options?: Partial<ActionOptions>): Action<TPayload, TResult> {
registerAction(name);

const tasks = new Set<Task<TResult>>();
const {
tasks,
} = registerAction(name);

const {
parallel,
Expand All @@ -93,11 +103,8 @@ export default function actionsExtension<TState extends BaseState>() {
const mutate = (mutator: Mutator<TState, undefined, void>) => _store.write(name, SENDER, mutator);

return ((payload: TPayload, controller?: AbortController) => {
if (!parallel && tasks.size > 0) {
tasks.forEach(task => {
task.abort();
tasks.delete(task);
});
if (!parallel) {
abortAction(name);
}

if (autoClearErrors) {
Expand Down Expand Up @@ -224,6 +231,21 @@ export default function actionsExtension<TState extends BaseState>() {
}));
}

function abortAction(name: string | string[]) {
([] as string[])
.concat(name)
.forEach(name => {
const tasks = actionTasks.get(name);

if (tasks && tasks.size > 0) {
tasks.forEach(task => {
task.abort();
tasks.delete(task);
});
}
});
}

const onBeforeAction = getActionTrigger(EVENTS.action.before);
const onAfterAction = getActionTrigger(EVENTS.action.after);
const onActionSuccess = getActionTrigger(EVENTS.action.success);
Expand All @@ -237,6 +259,7 @@ export default function actionsExtension<TState extends BaseState>() {
hasActionFailed,
getActionErrors,
resetActionState,
abortAction,
onBeforeAction,
onAfterAction,
onActionSuccess,
Expand Down
30 changes: 29 additions & 1 deletion extensions/action/test/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe('Actions Extension', () => {
expect(hasActionRun(loadUserInfoName)).toBe(true);
});

test('Handles cancellation', async () => {
test('Handles direct cancellation', async () => {
const {
loadUserInfo,
loadUserInfoName,
Expand All @@ -126,6 +126,34 @@ describe('Actions Extension', () => {
}
});

test('Handles indirect cancellation', async () => {
const {
loadUserInfo,
loadUserInfoName,
} = instance;

const {
state,
hasActionRun,
abortAction,
} = instance.store;

const task = loadUserInfo();

setTimeout(() => abortAction(loadUserInfoName), 100);

try {
await task;
} catch (error) {
expect(error).toBeInstanceOf(ActionAbortError);
} finally {
expect(state.details.firstName).toBe('');
expect(state.details.lastName).toBe('');
expect(state.details.age).toBe(0);
expect(hasActionRun(loadUserInfoName)).toBe(false);
}
});

test('Handles concurrency', async () => {
const {
action,
Expand Down

0 comments on commit a16f083

Please sign in to comment.