Skip to content

Commit

Permalink
supressIntermidiateErrors to retry (#340)
Browse files Browse the repository at this point in the history
* Add param supressIntermidiateErrors to retry operator

* Add comment

* Improve test and show bug

* Next steps of solution

* Solved

* Simplify solution

* Add option `supressIntermediateErrors` to `retry` operator

* INcrease size-limit

* Fix typo

* Allow to use otherwise and supressIntermediateErrors

* Improve docs
  • Loading branch information
igorkamyshev authored Aug 23, 2023
1 parent f77f6aa commit 1ec644e
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/spotty-poems-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@farfetched/core': minor
---

Add option `supressIntermediateErrors` to `retry` operator
1 change: 1 addition & 0 deletions apps/website/docs/api/operators/retry.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Config fields:
- `(params, { attempt }) => mapped`
- `{ source: Store, fn: (params, { attempt }, source) => mapped }`
- `otherwise?`: [_Event_](https://effector.dev/docs/api/effector/event) or [_Effect_](https://effector.dev/docs/api/effector/effect), that will be called after the last attempt if the [_Query_](/api/primitives/query) is still failed
- `supressIntermediateErrors?`: <Badge type="tip" text="since v0.9.0" /> _boolean_ whether to suppress intermediate errors or not, defaults to `false`. If `true`, then the [_Query_](/api/primitives/query) will not be marked as failed until the last attempt is failed.

## Build-in delays

Expand Down
16 changes: 15 additions & 1 deletion apps/website/docs/releases/0-9.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ Since v0.9 is technical release with no new significant features, there are a fe

### Separate [_Event_](https://effector.dev/docs/api/effector/event) for [_Query_](/api/primitives/query) cancelation

Cancelled [_Queries_](/api/primitives/query) were treated as failed before this release. We have added a separate [_Event_](https://effector.dev/docs/api/effector/event) `.aborted` to [_Query_](/api/primitives/query) to distinguish between cancelation and failure. It is recommended to use `.aborted` instead of `.finished.failure` to handle cancelation. In the next release v0.10 cancelation will not be treated as failure anymore, so you will have to handle it explicitly.
Cancelled [_Queries_](/api/primitives/query) were treated as failed before this release. We have added a separate [_Event_](https://effector.dev/docs/api/effector/event) `.aborted` to [_Query_](/api/primitives/query) to distinguish between cancelation and failure. It is recommended to use `.aborted` instead of `.finished.failure` to handle cancelation.

:::warning
In the next release v0.10 cancelation will not be treated as failure anymore, so you will have to handle it explicitly.
:::

### `supressIntermediateErrors` in `retry` operator

Before this release, [`retry`](/api/operators/retry) operator was marking [_Query_](/api/primitives/query) as failed on every failed attempt. Since v0.9 it accepts options `supressIntermediateErrors` to overwrite this behavior. If `true`, then the [_Query_](/api/primitives/query) will not be marked as failed until the last attempt is failed.

:::warning

In the next release v0.10 `supressIntermediateErrors` will be true `true` by default. To restore the previous behavior, you will have to set it to `false` explicitly.

:::

<!--@include: ./0-9.changelog.md-->
16 changes: 16 additions & 0 deletions apps/website/docs/tutorial/retries.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ retry(characterQuery, {

`mapParams` accepts a function that takes current parameters, occurred error and attempt number and returns new parameters. In this example, we just add `attempt` parameter to the current parameters.

## Intermediate errors

By default, `retry` does not supress errors, so if the operation failed, it will fail as well. But sometimes, we want to suppress errors and check for status only after all retries are failed. To do this, we can use the `supressIntermediateErrors` option of the `retry` operator.

```ts
retry(characterQuery, {
times: 5,
delay: 500,
supressIntermediateErrors: true,
});
```

::: tip
In the next release v0.10 `supressIntermediateErrors` will be true `true` by default. To restore the previous behavior, you will have to set it to `false` explicitly. If you want to be ready for this change, you can set it to `false` already.
:::

## API reference

You can find the full API reference for the `retry` operator in the [API reference](/api/operators/retry).
2 changes: 1 addition & 1 deletion packages/core/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"size": {
"executor": "./tools/executors/size-limit:size-limit",
"options": {
"limit": "18.9 kB",
"limit": "20 kB",
"outputPath": "dist/packages/core"
},
"dependsOn": [
Expand Down
40 changes: 25 additions & 15 deletions packages/core/src/remote_operation/create_remote_operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ export function createRemoteOperation<
unknown
>(async ({ params }) => {
const result = await executeFx(params);

return { result, stale: false };
}),
};
Expand Down Expand Up @@ -229,12 +228,13 @@ export function createRemoteOperation<

sample({
clock: retrieveDataFx.fail,
fn: ({ error, params }) => ({
error: error,
source: $enabled,
filter: (enabled, { error }) => enabled && !error.stopErrorPropagation,
fn: (_, { error, params }) => ({
error: error.error as any,
params: params.params,
meta: { stopErrorPropagation: false, stale: false },
meta: { stopErrorPropagation: error.stopErrorPropagation, stale: false },
}),
filter: $enabled,
target: finished.failure,
});

Expand Down Expand Up @@ -374,22 +374,32 @@ function createDataSourceHandlers<Params>(dataSources: DataSource<Params>[]) {
meta: ExecutionMeta;
},
{ result: unknown; stale: boolean },
any
{ stopErrorPropagation: boolean; error: unknown }
>({
handler: async ({ params, skipStale }) => {
for (const dataSource of dataSources) {
const fromSource = await dataSource.get({ params });

if (skipStale && fromSource?.stale) {
continue;
}

if (fromSource) {
return fromSource;
try {
const fromSource = await dataSource.get({ params });

if (skipStale && fromSource?.stale) {
continue;
}

if (fromSource) {
return fromSource;
}
} catch (error) {
throw {
stopErrorPropagation: false,
error,
};
}
}

throw new Error('No data source returned data');
throw {
stopErrorPropagation: false,
error: new Error('No data source returned data'),
};
},
});

Expand Down
40 changes: 40 additions & 0 deletions packages/core/src/retry/__tests__/retry.query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,44 @@ describe('retry with query', () => {
// 1 for retry
expect(listeners.onFailure).toBeCalledTimes(2);
});

test('throw error in case of retry with supressIntermediateErrors', async () => {
const query = createQuery({
handler: vi.fn().mockImplementation(({ attempt }) => {
throw new Error(`Sorry, attempt ${attempt}`);
}),
});

retry(query, {
times: 1,
mapParams({ meta }) {
return { attempt: meta.attempt };
},
delay: 0,
supressIntermediateErrors: true,
});

const scope = fork();

const { listeners } = watchRemoteOperation(query, scope);

await allSettled(query.start, { scope, params: { attempt: 0 } });

// 1 for retry
expect(listeners.onFailure).toBeCalledTimes(1);
expect(listeners.onFailure.mock.calls[0]).toMatchInlineSnapshot(`
[
{
"error": [Error: Sorry, attempt 1],
"meta": {
"stale": false,
"stopErrorPropagation": false,
},
"params": {
"attempt": 1,
},
},
]
`);
});
});
67 changes: 62 additions & 5 deletions packages/core/src/retry/retry.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import {
combine,
createEffect,
createEvent,
createStore,
sample,
split,
attach,
scopeBind,
type Event,
type EffectError,
type EffectParams,
type EffectResult,
} from 'effector';

import {
Expand Down Expand Up @@ -44,6 +50,7 @@ type RetryConfig<
MapParamsSource
>;
otherwise?: Event<FailInfo<Q>>;
supressIntermediateErrors?: boolean;
};

export function retry<
Expand All @@ -58,9 +65,11 @@ export function retry<
delay: timeout,
filter,
mapParams,
otherwise,
...params
}: RetryConfig<Q, DelaySource, FilterSource, MapParamsSource>
): void {
const supressIntermediateErrors = params.supressIntermediateErrors ?? false;

const $maxAttempts = normalizeStaticOrReactive(times);
const $attempt = createStore(1, {
serialize: 'ignore',
Expand All @@ -70,18 +79,30 @@ export function retry<
attempt: $attempt,
});

const $supressError = combine(
$attempt,
$maxAttempts,
(attempt, maxAttempts) =>
supressIntermediateErrors && attempt <= maxAttempts
);

const failed = createEvent<{
params: RemoteOperationParams<Q>;
error: RemoteOperationError<Q>;
}>();

const newAttempt = createEvent();

const { planNextAttempt, __: retriesAreOver } = split(
sample({
clock: operation.finished.failure,
clock: failed,
source: {
maxAttempts: $maxAttempts,
attempt: $attempt,
},
filter: normalizeSourced({
field: (filter ?? true) as any,
clock: operation.finished.failure,
clock: failed,
}),
fn: ({ attempt, maxAttempts }, { params, error }) => ({
params,
Expand Down Expand Up @@ -117,7 +138,43 @@ export function retry<
.on(newAttempt, (attempt) => attempt + 1)
.reset([operation.finished.success, operation.start]);

if (otherwise) {
sample({ clock: retriesAreOver, target: otherwise });
if (params.otherwise) {
sample({ clock: retriesAreOver, target: params.otherwise });
}

if (supressIntermediateErrors) {
const originalFx =
operation.__.lowLevelAPI.dataSourceRetrieverFx.use.getCurrent();

operation.__.lowLevelAPI.dataSourceRetrieverFx.use(
attach({
source: $supressError,
mapParams: (opts, supressError) => ({ ...opts, supressError }),
effect: createEffect<
EffectParams<
typeof operation.__.lowLevelAPI.dataSourceRetrieverFx
> & { supressError: boolean },
EffectResult<typeof operation.__.lowLevelAPI.dataSourceRetrieverFx>,
EffectError<typeof operation.__.lowLevelAPI.dataSourceRetrieverFx>
>(async ({ supressError, ...opts }) => {
const boundFailed = scopeBind(failed, { safe: true });
try {
const result = await originalFx(opts);

return result;
} catch (error: any) {
if (supressError) {
boundFailed({ params: opts.params, error: error.error });

throw { error: error.error, stopErrorPropagation: true };
} else {
throw error;
}
}
}),
})
);
}

sample({ clock: operation.finished.failure, target: failed });
}

0 comments on commit 1ec644e

Please sign in to comment.