Created
January 20, 2026 11:38
-
-
Save dovranJorayev/04f18dd883d9efe4c79fce4cebc56674 to your computer and use it in GitHub Desktop.
fetch based, JSON responded effect factory to create effects that can be used as is and as part of farfetched remote operation
This file contains hidden or 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 { | |
| configurationError, | |
| Contract, | |
| httpError, | |
| HttpError, | |
| invalidDataError, | |
| InvalidDataError, | |
| networkError, | |
| NetworkError, | |
| onAbort, | |
| preparationError, | |
| PreparationError, | |
| } from '@farfetched/core'; | |
| import { createEffect } from 'effector'; | |
| export interface RequestConfig extends RequestInit { | |
| url: string | URL; | |
| } | |
| export type FetchError = | |
| | HttpError | |
| | PreparationError | |
| | NetworkError | |
| | InvalidDataError | |
| | Error; | |
| /** | |
| * Error creators below should be covered | |
| * @see https://ff.effector.dev/api/utils/error_creators.html#error-creators | |
| * | |
| * - [x] abortError (handled by farfetched in operation level) | |
| * - [x] configurationError | |
| * - [x] httpError | |
| * - [x] invalidDataError | |
| * - [x] networkError | |
| * - [x] timeoutError (handled by farfetched in operation level) | |
| * - [x] preparationError | |
| * | |
| * @see https://ff.effector.dev/api/utils/on_abort.html | |
| * | |
| * onAbort is passed to the effect | |
| * @example | |
| * ```ts | |
| * const getTasksFx = createJsonFetchEffect({ | |
| * contract: zodContract(GetTasksDataSchema), | |
| * request: (arg) => ({ | |
| * url: makeUrl('/api/v1/image/tasks'), | |
| * method: 'GET', | |
| * body: (arg) => ({ | |
| * url: arg.url, | |
| * }), | |
| * }), | |
| * }) | |
| * ``` | |
| * | |
| * Other helpfull links @see https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch | |
| */ | |
| export function createJsonFetchEffect<Params = void, Data = unknown>(config: { | |
| request: RequestConfig | ((params: Params) => RequestConfig); | |
| contract: Contract<unknown, Data>; | |
| extra?: { | |
| /** | |
| * @default true | |
| * @description controls does onAbort hook is applied to the effect | |
| * or not. If effect intended to use separately from operation, | |
| * should be set to false. | |
| */ | |
| onAbortHook?: boolean; | |
| }; | |
| }) { | |
| const fetchFx = createEffect<Params, Data, FetchError>(async (params) => { | |
| const disposers: Array<() => void> = []; | |
| const defaultedExtra = { | |
| onAbortHook: config.extra?.onAbortHook ?? true, | |
| } satisfies NonNullable<Required<typeof config.extra>>; | |
| try { | |
| let requestInfoWithUrl: RequestConfig; | |
| if (typeof config.request === 'function') { | |
| try { | |
| requestInfoWithUrl = config.request(params); | |
| } catch (error) { | |
| const unsafeError = error as { message?: string } | undefined; | |
| const message = | |
| typeof unsafeError?.message === 'string' | |
| ? unsafeError.message | |
| : 'Unknown error'; | |
| throw configurationError({ | |
| reason: message, | |
| validationErrors: [`Failed to create request due to: ${message}`], | |
| }); | |
| } | |
| } else { | |
| requestInfoWithUrl = config.request; | |
| } | |
| const { url, signal, ...requestInit } = requestInfoWithUrl; | |
| const abortCtrl = new AbortController(); | |
| if (defaultedExtra.onAbortHook) { | |
| try { | |
| onAbort(() => { | |
| abortCtrl.abort(); | |
| }); | |
| } catch { | |
| /** | |
| * Error thrown by farfetched/core suppressed by reason of make effect available | |
| * to use outside of operation level is flag is true. | |
| * It should not cause performace degradation issue because it | |
| * custom serializable error instead of Error based one | |
| */ | |
| } | |
| } | |
| if (signal) { | |
| const abortHandler = () => { | |
| if (abortCtrl.signal.aborted) return; | |
| abortCtrl.abort(signal.reason); | |
| }; | |
| signal.addEventListener('abort', abortHandler); | |
| disposers.push(() => { | |
| signal.removeEventListener('abort', abortHandler); | |
| }); | |
| } | |
| const request = new Request(url, { | |
| ...requestInit, | |
| signal: abortCtrl.signal, | |
| }); | |
| const response = await fetch(request).catch((cause) => { | |
| throw networkError({ | |
| cause, | |
| reason: 'Network error', | |
| }); | |
| }); | |
| const data = await response.json().catch((error) => { | |
| throw preparationError({ | |
| response: 'Bad JSON response', | |
| reason: | |
| typeof error?.message === 'string' | |
| ? error.message | |
| : 'Bad JSON response', | |
| }); | |
| }); | |
| if (!response.ok) { | |
| throw httpError({ | |
| status: response.status, | |
| statusText: response.statusText, | |
| response: data, | |
| }); | |
| } | |
| if (!config.contract.isData(data)) { | |
| throw invalidDataError({ | |
| response: data, | |
| validationErrors: config.contract.getErrorMessages(data), | |
| }); | |
| } | |
| return data; | |
| } finally { | |
| disposers.forEach((disposer) => disposer()); | |
| disposers.length = 0; | |
| } | |
| }); | |
| return fetchFx; | |
| } |
This file contains hidden or 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
| export const postFilesToStorageFx = createJsonFetchEffect({ | |
| request: ({ file, signal }: PostFilesToStorageArg) => { | |
| const formData = new FormData(); | |
| formData.set('file', file); | |
| return { | |
| url: makeUrl('/api/v1/image/upload'), | |
| method: 'POST', | |
| body: formData, | |
| signal, | |
| }; | |
| }, | |
| contract: zodContract(PostFilesToStorageDataSchema), | |
| extra: { | |
| onAbortHook: false, // if you want to use it as standalone effect along side with mutation use-case | |
| }, | |
| }); | |
| export const createPostFilesToStorageMutation = createFactory(() => | |
| createMutation({ effect: postFilesToStorageFx }), | |
| ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment