Skip to content

Instantly share code, notes, and snippets.

@NazariiShvets
Last active October 25, 2023 20:26
Show Gist options
  • Save NazariiShvets/29808a88648f23e7ef2e02eac936b210 to your computer and use it in GitHub Desktop.
Save NazariiShvets/29808a88648f23e7ef2e02eac936b210 to your computer and use it in GitHub Desktop.
farfetched/effect
import { EffectAdapter } from './adapter';
import type { Mock } from 'vitest';
import { afterEach, describe, expect, vi } from 'vitest';
import type { EventPayload, Subscription } from 'effector';
import {
allSettled,
createEffect,
createEvent,
createWatch,
fork,
sample
} from 'effector';
import { setTimeout } from 'timers/promises';
describe('EffectAdapter', () => {
const subs: Subscription[] = [];
describe('EffectAdapter.fromEvents', () => {
afterEach(() => {
subs.forEach((s) => s.unsubscribe());
subs.splice(0, subs.length);
});
// Test doesn't work because all effects resolve with first promise
it('should handle async parallel calls', async () => {
const started = Date.now();
const start = createEvent<number>();
const done = createEvent();
const fail = createEvent();
const call = vi.fn();
const runFx = EffectAdapter.fromEvents({
start,
done,
fail
});
const scope = fork();
{
subs.push(
createWatch({
scope,
fn: (payload) =>
call({ name: 'start', payload, timestamp: Date.now() - started }),
unit: start
}),
createWatch({
scope,
fn: (payload) =>
call({ name: 'done', payload, timestamp: Date.now() - started }),
unit: done
}),
createWatch({
scope,
fn: (payload) =>
call({ name: 'fail', payload, timestamp: Date.now() - started }),
unit: fail
})
);
}
{
const timeoutFx = createEffect({
handler: async (payload: EventPayload<typeof start>) => {
call({ name: 'handler', payload });
await setTimeout(payload);
return payload;
}
});
subs.push(
createWatch({
scope,
fn: (payload) =>
call({
name: 'fx.start',
payload,
timestamp: Date.now() - started
}),
unit: runFx
}),
createWatch({
scope,
fn: (payload) => {
call({
name: `fx.finally.${payload.status}`,
payload: payload,
timestamp: Date.now() - started
});
},
unit: runFx.finally
})
);
sample({
clock: start,
target: timeoutFx
});
sample({
clock: timeoutFx.doneData,
target: done
});
}
{
allSettled(runFx, { scope, params: 200 });
allSettled(runFx, { scope, params: 100 });
allSettled(runFx, { scope, params: 150 });
await allSettled(scope);
await setTimeout(1000);
// (!!!) ALL EFFECTS RESOLVED WITH FIRST PROMISE
expect(argumentHistory(call)).toMatchInlineSnapshot(`
[
{
"name": "fx.start",
"payload": 200,
"timestamp": 4,
},
{
"name": "start",
"payload": 200,
"timestamp": 5,
},
{
"name": "handler",
"payload": 200,
},
{
"name": "fx.start",
"payload": 100,
"timestamp": 5,
},
{
"name": "start",
"payload": 100,
"timestamp": 6,
},
{
"name": "handler",
"payload": 100,
},
{
"name": "fx.start",
"payload": 150,
"timestamp": 6,
},
{
"name": "start",
"payload": 150,
"timestamp": 7,
},
{
"name": "handler",
"payload": 150,
},
{
"name": "done",
"payload": 100,
"timestamp": 119,
},
{
"name": "fx.finally.done",
"payload": {
"params": 200,
"result": 100,
"status": "done",
},
"timestamp": 120,
},
{
"name": "fx.finally.done",
"payload": {
"params": 100,
"result": 100,
"status": "done",
},
"timestamp": 121,
},
{
"name": "fx.finally.done",
"payload": {
"params": 150,
"result": 100,
"status": "done",
},
"timestamp": 121,
},
{
"name": "done",
"payload": 150,
"timestamp": 169,
},
{
"name": "done",
"payload": 200,
"timestamp": 219,
},
]
`);
}
});
});
});
function argumentHistory(fn: Mock) {
return fn.mock.calls.map(([arg]) => arg);
}
/* eslint-disable effector/no-ambiguity-target */
import type { Event, Domain } from 'effector';
import {
clearNode,
createEffect,
createNode,
launch,
sample,
withRegion
} from 'effector';
import { v4 } from 'uuid';
type CallMeta = Record<string, any>;
class EffectAdapter {
/**
* This is a workaround to use effect API when only have events.
* (!!!) And you can't pass meta through them.
*
* @note Use this with caution, because it doesn't support parallel calls.
* So if you have two calls with the same params, all effects would be resolved with first done call.
*
*/
public static fromEvents<Params, Done, Fail = Error>(
{
start,
done,
fail
}: {
start: Event<Params>;
done: Event<Done>;
fail: Event<Fail>;
},
config: {
name?: string;
sid?: string;
domain?: Domain;
} = {}
) {
const name = config.name ?? 'unnamedRunFx';
const runFx = createEffect<Params, Done, Fail>({
...config,
name: name,
handler: (params) => {
// TODO: Fix parallel calls
const callId = v4();
return new Promise((resolve, reject) => {
const node = createNode({ meta: { callId } });
withRegion(node, () => {
const resolveFx = createEffect<Done, void, void>({
name: `${name}/resolveFx`,
handler: (result) => {
resolve(result);
clearNode(node);
}
});
const rejectFx = createEffect<Fail, void, void>({
name: `${name}/rejectFx`,
handler: (error) => {
reject(error);
clearNode(node);
}
});
sample({
clock: done,
// TODO: Fix parallel calls
// filter: ({ meta }) => meta.callId === callId,
target: resolveFx
});
sample({
clock: fail,
// TODO: Fix parallel
// filter: ({ meta }) => meta.callId === callId,
target: rejectFx
});
launch({ target: start, params, meta: { callId } });
});
});
}
});
return runFx;
}
}
export { EffectAdapter };
export type { CallMeta };
import { EffectAdapter } from '@acme/effector/effect/adapter';
import { Query } from '@farfetched/core';
import { freshChain } from '@farfetched/atomic-router';
import { Effect, Event, EventPayload } from 'effector';
const freshEffect = <TQuery extends Query<any, any, any, any>>(
query: TQuery
): Effect<
EventPayload<TQuery['refresh']>,
EventPayload<TQuery['finished']['success']>,
| EventPayload<TQuery['finished']['failure']>
| EventPayload<TQuery['finished']['skip']>
> => {
const { beforeOpen, openOn, cancelOn } = freshChain(query);
const freshFx = EffectAdapter.fromEvents(
{
start: beforeOpen.prepend((params: EventPayload<TQuery['refresh']>) => ({
params,
query: {}
})),
done: openOn as Event<EventPayload<TQuery['finished']['success']>>,
fail: cancelOn as Event<
| EventPayload<TQuery['finished']['failure']>
| EventPayload<TQuery['finished']['skip']>
>
},
{
name: `${query.__.meta.name}.freshFx`
}
);
return freshFx;
};
export { freshEffect };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment