Last active
October 25, 2023 20:26
-
-
Save NazariiShvets/29808a88648f23e7ef2e02eac936b210 to your computer and use it in GitHub Desktop.
farfetched/effect
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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