Skip to content

Commit 401bad6

Browse files
authored
Merge pull request #15 from BetterTyped/feature/send-event-callbacks
Feature/send event callbacks
2 parents b23bf96 + 933104d commit 401bad6

File tree

12 files changed

+174
-105
lines changed

12 files changed

+174
-105
lines changed

packages/core/__tests__/features/command/command.sending.spec.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,49 @@ describe("Command [ Sending ]", () => {
7676
it("should return error once request got removed", async () => {
7777
createRequestInterceptor(command, { delay: 10, status: 400 });
7878
const request = command.send();
79-
await sleep(5);
79+
await sleep(2);
8080

8181
const runningRequests = builder.fetchDispatcher.getAllRunningRequest();
8282
builder.fetchDispatcher.delete(command.queueKey, runningRequests[0].requestId, command.abortKey);
8383

8484
const response = await request;
85-
expect(response).toStrictEqual([null, getErrorMessage(), 0]);
85+
expect(response).toStrictEqual([null, getErrorMessage("deleted"), 0]);
8686
});
87-
it("should allow to settle the request", async () => {
87+
it("should call remove error", async () => {
8888
const spy = jest.fn();
89-
const response = await command.send({}, spy);
89+
createRequestInterceptor(command, { delay: 10, status: 400 });
90+
const request = command.send({ onRemove: spy });
91+
await sleep(2);
9092

91-
expect(response).toStrictEqual([fixture, null, 200]);
93+
const runningRequests = builder.fetchDispatcher.getAllRunningRequest();
94+
builder.fetchDispatcher.delete(command.queueKey, runningRequests[0].requestId, command.abortKey);
95+
96+
await request;
97+
expect(spy).toBeCalledTimes(1);
98+
});
99+
it("should allow to call the request callbacks", async () => {
100+
const spy1 = jest.fn();
101+
const spy2 = jest.fn();
102+
const spy3 = jest.fn();
103+
const spy4 = jest.fn();
104+
const spy5 = jest.fn();
105+
const spy6 = jest.fn();
106+
107+
await command.send({
108+
onSettle: spy1,
109+
onRequestStart: spy2,
110+
onResponseStart: spy3,
111+
onUploadProgress: spy4,
112+
onDownloadProgress: spy5,
113+
onResponse: spy6,
114+
});
115+
116+
expect(spy1).toBeCalledTimes(1);
117+
expect(spy2).toBeCalledTimes(1);
118+
expect(spy3).toBeCalledTimes(1);
119+
expect(spy4).toBeCalledTimes(2);
120+
expect(spy5).toBeCalledTimes(3);
121+
expect(spy6).toBeCalledTimes(1);
92122
});
93123
});
94124
});

packages/core/src/command/command.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
ExtractRouteParams,
1111
commandSendRequest,
1212
CommandCurrentType,
13-
CommandQueueOptions,
1413
} from "command";
1514
import { Builder } from "builder";
1615
import { getUniqueRequestId } from "utils";
@@ -433,8 +432,7 @@ export class Command<
433432
HasParams,
434433
HasQuery,
435434
MappedData
436-
>,
437-
CommandQueueOptions
435+
>
438436
> = async (
439437
options?: FetchType<
440438
Command<
@@ -449,25 +447,8 @@ export class Command<
449447
HasParams,
450448
HasQuery,
451449
MappedData
452-
>,
453-
CommandQueueOptions
450+
>
454451
>,
455-
onInit?: (
456-
requestId: string,
457-
command: Command<
458-
ResponseType,
459-
RequestDataType,
460-
QueryParamsType,
461-
GlobalErrorType,
462-
LocalErrorType,
463-
EndpointType,
464-
ClientOptions,
465-
HasData,
466-
HasParams,
467-
HasQuery,
468-
MappedData
469-
>,
470-
) => void,
471452
) => {
472453
const { dispatcherType, ...rest } = options || {};
473454

@@ -486,7 +467,7 @@ export class Command<
486467
HasQuery,
487468
MappedData
488469
>
489-
>(command, dispatcherType, onInit);
470+
>(command, options);
490471
};
491472
}
492473

packages/core/src/command/command.types.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
ExtractResponse,
1515
} from "types";
1616
import { Command } from "command";
17-
import { ClientResponseType, ClientQueryParamsType } from "client";
17+
import { ClientResponseType, ClientQueryParamsType, FetchProgressType } from "client";
18+
import { CommandEventDetails, CommandResponseDetails } from "managers";
1819

1920
// Progress
2021
export type ClientProgressEvent = { total: number; loaded: number };
@@ -233,36 +234,35 @@ export type CommandQueueOptions = {
233234
dispatcherType?: "auto" | "fetch" | "submit";
234235
};
235236

236-
export type FetchType<Command extends CommandInstance, AdditionalOptions = unknown> = FetchQueryParamsType<
237+
export type FetchType<Command extends CommandInstance> = FetchQueryParamsType<
237238
ExtractQueryParams<Command>,
238239
ExtractHasQueryParams<Command>
239240
> &
240241
FetchParamsType<ExtractEndpoint<Command>, ExtractHasParams<Command>> &
241242
FetchRequestDataType<ExtractRequestData<Command>, ExtractHasData<Command>> &
242-
FetchOptionsType<ExtractClientOptions<Command>> &
243-
AdditionalOptions;
243+
Omit<FetchOptionsType<ExtractClientOptions<Command>>, "params" | "data"> &
244+
FetchSendActionsType<Command> &
245+
CommandQueueOptions;
244246

245-
export type FetchMethodType<Command extends CommandInstance, AdditionalOptions = unknown> = FetchType<
246-
Command,
247-
AdditionalOptions
248-
>["data"] extends any
249-
? (
250-
options?: FetchType<Command, AdditionalOptions>,
251-
onInit?: (requestId: string, command: Command) => void,
252-
) => Promise<ClientResponseType<ExtractResponse<Command>, ExtractError<Command>>>
253-
: FetchType<Command, AdditionalOptions>["data"] extends NegativeTypes
254-
? FetchType<Command, AdditionalOptions>["params"] extends NegativeTypes
255-
? (
256-
options?: FetchType<Command, AdditionalOptions>,
257-
onInit?: (requestId: string, command: Command) => void,
258-
) => Promise<ClientResponseType<ExtractResponse<Command>, ExtractError<Command>>>
259-
: (
260-
options: FetchType<Command, AdditionalOptions>,
261-
onInit?: (requestId: string, command: Command) => void,
262-
) => Promise<ClientResponseType<ExtractResponse<Command>, ExtractError<Command>>>
263-
: (
264-
options: FetchType<Command, ExtractHasQueryParams<Command>>,
265-
onInit?: (requestId: string, command: Command) => void,
266-
) => Promise<ClientResponseType<ExtractResponse<Command>, ExtractError<Command>>>;
247+
export type FetchSendActionsType<Command extends CommandInstance> = {
248+
onSettle?: (requestId: string, command: Command) => void;
249+
onRequestStart?: (details: CommandEventDetails<Command>) => void;
250+
onResponseStart?: (details: CommandEventDetails<Command>) => void;
251+
onUploadProgress?: (values: FetchProgressType, details: CommandEventDetails<Command>) => void;
252+
onDownloadProgress?: (values: FetchProgressType, details: CommandEventDetails<Command>) => void;
253+
onResponse?: (
254+
response: ClientResponseType<ExtractResponse<Command>, ExtractError<Command>>,
255+
details: CommandResponseDetails,
256+
) => void;
257+
onRemove?: (details: CommandEventDetails<Command>) => void;
258+
};
259+
260+
export type FetchMethodType<Command extends CommandInstance> = FetchType<Command>["data"] extends any
261+
? (options?: FetchType<Command>) => Promise<ClientResponseType<ExtractResponse<Command>, ExtractError<Command>>>
262+
: FetchType<Command>["data"] extends NegativeTypes
263+
? FetchType<Command>["params"] extends NegativeTypes
264+
? (options?: FetchType<Command>) => Promise<ClientResponseType<ExtractResponse<Command>, ExtractError<Command>>>
265+
: (options: FetchType<Command>) => Promise<ClientResponseType<ExtractResponse<Command>, ExtractError<Command>>>
266+
: (options: FetchType<Command>) => Promise<ClientResponseType<ExtractResponse<Command>, ExtractError<Command>>>;
267267

268268
export type CommandInstance = Command<any, any, any, any, any, any, any, any, any, any, any>;

packages/core/src/command/command.utils.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FetchProgressType, ClientResponseType, getErrorMessage } from "client";
2-
import { ClientProgressEvent, CommandInstance, CommandDump } from "command";
2+
import { ClientProgressEvent, CommandInstance, CommandDump, FetchType } from "command";
33
import { HttpMethodsEnum } from "constants/http.constants";
44
import { canRetryRequest, Dispatcher, isFailedRequest } from "dispatcher";
55
import { ExtractError, ExtractResponse } from "types";
@@ -110,20 +110,32 @@ export const getCommandDispatcher = <Command extends CommandInstance>(
110110
return [dispatcher, isFetchDispatcher];
111111
};
112112

113-
export const commandSendRequest = <T extends CommandInstance>(
114-
command: T,
115-
dispatcherType: "auto" | "fetch" | "submit" = "auto",
116-
settleCallback?: (requestId: string, command: T) => void,
117-
) => {
113+
export const commandSendRequest = <Command extends CommandInstance>(command: Command, options?: FetchType<Command>) => {
118114
const { commandManager } = command.builder;
119-
const [dispatcher] = getCommandDispatcher(command, dispatcherType);
115+
const [dispatcher] = getCommandDispatcher(command, options?.dispatcherType);
120116

121-
return new Promise<ClientResponseType<ExtractResponse<T>, ExtractError<T>>>((resolve) => {
117+
return new Promise<ClientResponseType<ExtractResponse<Command>, ExtractError<Command>>>((resolve) => {
122118
const requestId = dispatcher.add(command);
123-
settleCallback?.(requestId, command);
119+
options?.onSettle?.(requestId, command);
120+
121+
const unmountRequestStart = commandManager.events.onRequestStartById<Command>(requestId, (...props) =>
122+
options?.onRequestStart?.(...props),
123+
);
124+
125+
const unmountResponseStart = commandManager.events.onResponseStartById<Command>(requestId, (...props) =>
126+
options?.onResponseStart?.(...props),
127+
);
128+
129+
const unmountUpload = commandManager.events.onUploadProgressById<Command>(requestId, (...props) =>
130+
options?.onUploadProgress?.(...props),
131+
);
132+
133+
const unmountDownload = commandManager.events.onDownloadProgressById<Command>(requestId, (...props) =>
134+
options?.onDownloadProgress?.(...props),
135+
);
124136

125137
// When resolved
126-
const unmountResponse = commandManager.events.onResponseById<ExtractResponse<T>, ExtractError<T>>(
138+
const unmountResponse = commandManager.events.onResponseById<ExtractResponse<Command>, ExtractError<Command>>(
127139
requestId,
128140
(response, details) => {
129141
const isFailed = isFailedRequest(response);
@@ -136,20 +148,32 @@ export const commandSendRequest = <T extends CommandInstance>(
136148
// When command is in retry mode we need to listen for retries end
137149
if (isFailed && willRetry) return;
138150

151+
options?.onResponse?.(response, details);
139152
resolve(response);
140153

141154
// Unmount Listeners
142-
unmountResponse();
155+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
156+
umountAll();
143157
},
144158
);
145159

146160
// When removed from queue storage we need to clean event listeners and return proper error
147-
const unmountRemoveQueueElement = commandManager.events.onRemoveById(requestId, () => {
148-
resolve([null, getErrorMessage("deleted") as unknown as ExtractError<T>, 0]);
161+
const unmountRemoveQueueElement = commandManager.events.onRemoveById<Command>(requestId, (...props) => {
162+
options?.onRemove?.(...props);
163+
resolve([null, getErrorMessage("deleted") as unknown as ExtractError<Command>, 0]);
149164

150165
// Unmount Listeners
166+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
167+
umountAll();
168+
});
169+
170+
function umountAll() {
171+
unmountRequestStart();
172+
unmountResponseStart();
173+
unmountUpload();
174+
unmountDownload();
151175
unmountResponse();
152176
unmountRemoveQueueElement();
153-
});
177+
}
154178
});
155179
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { getRefreshTime } from "use-fetch";
2+
3+
describe("useFetch [ Utils ]", () => {
4+
const refreshTime = 1000;
5+
describe("given using getRefreshTime util", () => {
6+
describe("when state is available", () => {
7+
it("should give the reduced refresh time based on timestamp", async () => {
8+
const timestamp = new Date(+new Date() - 500);
9+
const time = getRefreshTime(refreshTime, timestamp);
10+
expect(time).toBeLessThanOrEqual(500);
11+
expect(time).toBeGreaterThanOrEqual(490);
12+
});
13+
it("should give refreshTime on old timestamp", async () => {
14+
const timestamp = new Date(+new Date() - refreshTime);
15+
const time = getRefreshTime(refreshTime, timestamp);
16+
expect(time).toBe(refreshTime);
17+
});
18+
});
19+
describe("when only refreshTime gets provided", () => {
20+
it("should give refreshTime", async () => {
21+
const time = getRefreshTime(refreshTime);
22+
expect(time).toBe(refreshTime);
23+
});
24+
});
25+
});
26+
});

packages/react/__tests__/features/use-submit/use-submit.submit.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ describe("useSubmit [ Base ]", () => {
3636

3737
expect(data).toStrictEqual([mock, null, 200]);
3838
});
39+
it("should call onSettle", async () => {
40+
const spy = jest.fn();
41+
createRequestInterceptor(command);
42+
const response = renderUseSubmit(command);
43+
44+
await act(async () => {
45+
await response.result.current.submit({ onSettle: spy });
46+
});
47+
48+
expect(spy).toBeCalledTimes(1);
49+
});
3950
it("should return data from submit method on retries", async () => {
4051
let data: unknown = null;
4152
let mock: unknown = {};

packages/react/src/helpers/use-command-events/use-command-events.hooks.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
ExtractError,
44
ExtractResponse,
55
CommandInstance,
6-
getErrorMessage,
76
FetchProgressType,
87
ClientResponseType,
98
CommandEventDetails,
@@ -144,24 +143,6 @@ export const useCommandEvents = <T extends CommandInstance>({
144143
removeLifecycleListener(requestId);
145144
};
146145

147-
const handleAbort = (cmd: T) => {
148-
return () => {
149-
const data: ClientResponseType<ExtractResponse<T>, ExtractError<T>> = [
150-
null,
151-
getErrorMessage("abort") as ExtractError<T>,
152-
0,
153-
];
154-
const details: CommandResponseDetails = {
155-
retries: 0,
156-
timestamp: +new Date(),
157-
isFailed: false,
158-
isCanceled: true,
159-
isOffline: false,
160-
};
161-
handleResponseCallbacks(cmd, data, details);
162-
};
163-
};
164-
165146
// ******************
166147
// Data Listeners
167148
// ******************
@@ -175,12 +156,10 @@ export const useCommandEvents = <T extends CommandInstance>({
175156
// Data handlers
176157
const loadingUnmount = commandManager.events.onLoading(cmd.queueKey, handleGetLoadingEvent(cmd.queueKey));
177158
const getResponseUnmount = cache.events.onData<ExtractResponse<T>, ExtractError<T>>(cmd.cacheKey, setCacheData);
178-
const abortUnmount = commandManager.events.onAbort(cmd.abortKey, handleAbort(cmd));
179159

180160
const unmount = () => {
181161
loadingUnmount();
182162
getResponseUnmount();
183-
abortUnmount();
184163
};
185164

186165
clearDataListener();
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from "./use-fetch.hooks";
22
export * from "./use-fetch.types";
3+
export * from "./use-fetch.utils";
34
export * from "./use-fetch.constants";

packages/react/src/use-fetch/use-fetch.hooks.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useDidUpdate, useDidMount } from "@better-typed/react-lifecycle-hooks";
33
import { CommandInstance, Command, getCommandKey } from "@better-typed/hyper-fetch";
44

55
import { useDebounce, useCommandEvents, useTrackedState } from "helpers";
6-
import { UseFetchOptionsType, useFetchDefaultOptions, UseFetchReturnType } from "use-fetch";
6+
import { UseFetchOptionsType, useFetchDefaultOptions, UseFetchReturnType, getRefreshTime } from "use-fetch";
77
import { useConfigProvider } from "config-provider";
88

99
/**
@@ -91,7 +91,9 @@ export const useFetch = <T extends CommandInstance>(
9191

9292
function handleRefresh() {
9393
if (!refresh) return;
94-
logger.debug(`Starting refresh counter, request will be send in ${refreshTime}ms`);
94+
const time = getRefreshTime(refreshTime, state.timestamp);
95+
96+
logger.debug(`Starting refresh counter, request will be send in ${time}ms`);
9597

9698
refreshDebounce.debounce(() => {
9799
const isBlurred = !appManager.isFocused;
@@ -110,7 +112,7 @@ export const useFetch = <T extends CommandInstance>(
110112

111113
// Start new refresh counter
112114
handleRefresh();
113-
});
115+
}, time);
114116
}
115117

116118
const revalidate = (invalidateKey?: string | CommandInstance | RegExp) => {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const getRefreshTime = (refreshTime: number, dataTimestamp?: Date) => {
2+
if (dataTimestamp) {
3+
const timeDiff = Date.now() - +dataTimestamp;
4+
return timeDiff < refreshTime ? refreshTime - timeDiff : refreshTime;
5+
}
6+
return refreshTime;
7+
};

0 commit comments

Comments
 (0)