Skip to content

Instantly share code, notes, and snippets.

@NazariiShvets
Last active December 17, 2023 09:41
Show Gist options
  • Save NazariiShvets/977cfa53e70e5965be6839aefb249ae8 to your computer and use it in GitHub Desktop.
Save NazariiShvets/977cfa53e70e5965be6839aefb249ae8 to your computer and use it in GitHub Desktop.
effector-listen
/**
* Utility for calling imperative effects
* without need of creating one
*
* Awaiting promise instead of effect would lose scope safety
* @see https://effector.dev/docs/api/effector/scope/#imperative-effects-calls-with-scope
*/
const callFx = createEffect({
name: 'callFx',
handler: async <T extends (arg: any) => any>({
fn,
params
}: {
fn: T;
params: Parameters<T>[0];
}): Promise<ReturnType<T>> => fn(params)
});
/**
* Calls provided function with provided params inside effect
*
* @param fn
* @param params
*/
const call = <T extends (...args: any[]) => any>(
fn: T,
params: Parameters<T>[0]
): ReturnType<T> => {
const result = callFx({ fn, params });
return result as ReturnType<T>;
};
export { call }
const delayFx = createEffect({
name: 'delayFx',
handler: async (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms))
});
export { delay }
const name = config?.name ?? 'unnamed.$$foo'
listen({
name: `${name}.onInit`,
clock: init,
source: {
ctx: config.$ctx,
paymentMethod: config.$selectedPaymentMethod
},
handler: async (_, { ctx, paymentMethod }) => {
try {
invariant.error(paymentMethod, 'Payment method is not selected');
if (!paymentMethod.isPrefill) {
$$requisiteForm.$$form.put(
Object.fromEntries(
Object.keys(paymentMethod.fields).map((key) => [key, ''])
)
)
return;
}
const prefillValues = await config.getAssetPaymentMethodPrefillFx({
asset: paymentMethod.Asset.code,
code: paymentMethod.code,
type: paymentMethod.transferType
});
const values = await getState($$requisiteForm.$$form.$values);
$$requisiteForm.$$form.put({ ...values, ...prefillValues });
} catch (e) {
ctx.notify.warning(
ctx.i18n.t('model.requisite-create.get-prefill.failed')
);
//re-throw for debugging
throw e;
}
},
debug: import.meta.env.DEV
})
listen({
clock: Gate.open,
handler: () => {
$$table.$$load.start();
}
});
listen({
name: 'Poll reports while in progress',
clock: $$table.$$load.done,
handler: (rows) => {
const boundRemoveIntervalId = scopeBind(removeIntervalId, {
safe: true
});
let inProgressReportIds = rows
.filter((row) => row.status === ReportStatus.InProgress)
.map((report) => report.id);
if (inProgressReportIds.length === 0) return;
const intervalId = setInterval(async () => {
const results = await call(findManyReport(SELECT), {
take: inProgressReportIds.length,
where: {
type: {
equals: ReportType.Statement
},
id: {
in: inProgressReportIds
},
createdAt: {
gte: subDays(startOfToday(), 7)
}
}
});
results
.filter((row) => row.status !== ReportStatus.InProgress)
.forEach((report) => {
$$table.$$row.replaceRow({
comparator: (row) => row.id === report.id,
data: report
});
});
inProgressReportIds = results
.filter((row) => row.status === ReportStatus.InProgress)
.map((report) => report.id);
if (inProgressReportIds.length === 0) {
clearInterval(intervalId);
boundRemoveIntervalId(intervalId);
}
}, minutesToMilliseconds(1));
addIntervalId(intervalId);
}
});
listen({
clock: Gate.close,
source: $intervalIds,
handler: (_, intervals) => {
intervals.forEach((interval) => clearInterval(interval));
setState($intervalIds, []);
$$table.reset();
}
});
/**
* Operator for listening events to run imperative effects
*
* @see https://redux-toolkit.js.org/api/createListenerMiddleware#startlistening
*/
function listen<TClock, TSource>(config: ListenConfig<TClock, TSource>) {
const $source =
config.source ??
createStore(null, {
name: `${config.name}.$source`,
serialize: 'ignore'
});
sample({
clock: config.clock,
target: attach({
name: config.name,
source: $source,
effect: async (source, clock: TClock) => {
if (config.debug) {
console.log(`[listen](start) ${config.name}`, { clock, source });
}
const result = config.handler(
clock,
source as ListenConfigEffectData<TSource>
);
if (config.debug) {
if (result instanceof Promise) {
result
.then((value) =>
console.log(`[listen](done) ${config.name}`, {
clock,
source,
result: value
})
)
.catch((error) =>
console.log(`[listen](error) ${config.name}`, {
clock,
source,
error
})
);
} else {
console.log(`[listen](done) ${config.name}`, {
clock,
source,
result
});
}
}
}
})
});
}
export { listen }
type ListenConfig<TClock, TSource> = {
/**
* Name of the handler
*/
name?: string;
/**
* When this event is triggered, the effect will be run
*/
clock: Event<TClock>;
/**
* Custom data for the effect
*/
source?: TSource;
/**
* Effect to run
*
* @note Name `handler` is used to avoid name collision with `effect` because effector-babel-plugin goes nuts
*/
handler: (
clock: TClock,
data: ListenConfigEffectData<TSource>
) => void | Promise<void>;
debug?: boolean;
};
type ListenConfigEffectData<TSource> = TSource extends Record<
string,
Store<any>
>
? GetShapeValue<TSource>
: TSource extends Store<any>
? GetShapeValue<TSource>
: void;
export { ListenConfig, ListenConfigEffectData }
/**
* Handy utility for imperative setting store state
*/
const setState = <TValue>(store: Store<TValue>, value: TValue) => {
launch({ target: store, params: value });
};
const getStateFx = createEffect(<T>(store: Store<T>) => store.getState())
async function getState<TValue>(store: Store<TValue>): Promise<TValue> {
const value = await getStateFx(store);
return value
}
export { setState, getState }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment