Skip to content

Commit

Permalink
feat(action): added custom strategy options to action extension
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewcourtice committed Feb 16, 2022
1 parent 6ae0ad6 commit cf49768
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 12 deletions.
6 changes: 4 additions & 2 deletions extensions/action/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export class ActionAbortError extends Error {
public name: string;
public instanceId: symbol;
public reason?: unknown;

constructor(name: string, instanceId: symbol) {
super(`Action ${name} as been cancelled`);
constructor(name: string, instanceId: symbol, reason: unknown = 'unknown') {
super(`Action ${name} as been cancelled. Reason: ${reason}`);

this.name = name;
this.instanceId = instanceId;
this.reason = reason;
}
}
38 changes: 30 additions & 8 deletions extensions/action/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ import {

import type {
Action,
ActionAbortStrategies,
ActionBody,
ActionEventData,
ActionHookHandler,
ActionOptions,
ActionPredicate,
ActionStoreState,
Options,
} from './types';

export {
Expand All @@ -37,7 +39,27 @@ export {

export * from './types';

export default function actionsExtension<TState extends BaseState>() {
export const ABORT_STRATEGY = {
error: (name, id, resolve, reject, reason) => {
reject(new ActionAbortError(name, id, reason));
},
warn: (name, id, resolve, reject, reason) => {
console.warn(`Action ${name} has been cancelled. Reason: ${reason || 'unknown'}`);
resolve();
},
} as ActionAbortStrategies;

export default function actionsExtension<TState extends BaseState>(options?: Partial<Options>) {
const {
strategies,
} = {
...options,
strategies: {
abort: ABORT_STRATEGY.error,
...options?.strategies,
},
};

return (store: InternalStore<TState>) => {
const _store = store as unknown as InternalStore<TState & ActionStoreState>;

Expand Down Expand Up @@ -98,13 +120,13 @@ export default function actionsExtension<TState extends BaseState>() {
parallel: false,
autoClearErrors: true,
...options,
};
} as ActionOptions;

const mutate = (mutator: Mutator<TState, undefined, void>) => _store.write(name, SENDER, mutator);

return ((payload: TPayload, controller?: AbortController) => {
if (!parallel) {
abortAction(name);
abortAction(name, 'New instance started on non-parallel action');
}

if (autoClearErrors) {
Expand All @@ -115,7 +137,7 @@ export default function actionsExtension<TState extends BaseState>() {
const id = Symbol(name);

const complete = () => (tasks.delete(task), removeInstance(name, id));
const fail = () => reject(new ActionAbortError(name, id));
const fail = (reason?: unknown) => strategies.abort(name, id, resolve, reject, reason);

let result: TResult | undefined;

Expand All @@ -125,7 +147,7 @@ export default function actionsExtension<TState extends BaseState>() {
result,
} as ActionEventData);

onAbort(() => (complete(), fail()));
onAbort(reason => (complete(), fail(reason)));
addInstance(name, id, payload);

emit(EVENTS.action.before);
Expand All @@ -140,7 +162,7 @@ export default function actionsExtension<TState extends BaseState>() {
resolve(result);
} catch (error) {
if (error instanceof DOMException) {
return fail(); // Fetch has been cancelled
return fail('Network request cancelled'); // Fetch has been cancelled
}

emit(EVENTS.action.error);
Expand Down Expand Up @@ -235,15 +257,15 @@ export default function actionsExtension<TState extends BaseState>() {
}));
}

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

if (tasks && tasks.size > 0) {
tasks.forEach(task => {
task.abort();
task.abort(reason);
tasks.delete(task);
});
}
Expand Down
13 changes: 13 additions & 0 deletions extensions/action/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ export type ActionBody<TState extends BaseState, TPayload = undefined, TResult =
export type Action<TPayload, TResult = void> = undefined extends TPayload ? (payload?: TPayload, controller?: AbortController) => Task<TResult> : (payload: TPayload, controller?: AbortController) => Task<TResult>;
export type ActionPredicate<TPayload = unknown> = (payload?: TPayload) => boolean;
export type ActionHookHandler<TPayload, TResult> = (data: ActionEventData<TPayload, TResult>) => void;
export type ActionAbortStrategy = (name: string, id: symbol, resolve: (value?: any) => void, reject: (reason: unknown) => void, reason?: unknown) => void;

export interface Options {
strategies: {
abort: ActionAbortStrategy
}
}

export interface ActionAbortStrategies {
error: ActionAbortStrategy;
warn: ActionAbortStrategy;
}

export interface ActionTaskState {
runCount: number;
Expand All @@ -25,6 +37,7 @@ export interface ActionStoreState {
export interface ActionOptions {
parallel: boolean;
autoClearErrors: boolean;
suppressAbortErrors: boolean;
}

export interface ActionEventData<TPayload = any, TResult = any> {
Expand Down
8 changes: 6 additions & 2 deletions extensions/action/test/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ describe('Actions Extension', () => {

const task = loadUserInfo();

setTimeout(() => task.abort(), 100);
setTimeout(() => task.abort('Direct cancellation'), 100);

expect.assertions(5);

try {
await task;
Expand Down Expand Up @@ -140,7 +142,9 @@ describe('Actions Extension', () => {

const task = loadUserInfo();

setTimeout(() => abortAction(loadUserInfoName), 100);
setTimeout(() => abortAction(loadUserInfoName, 'Indirect cancellation'), 100);

expect.assertions(5);

try {
await task;
Expand Down

0 comments on commit cf49768

Please sign in to comment.