Skip to content

Commit dcb6a57

Browse files
committed
✨ [util] 新增func_throttle
1 parent 258e4da commit dcb6a57

File tree

8 files changed

+246
-46
lines changed

8 files changed

+246
-46
lines changed

deno.lock

Lines changed: 1 addition & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

util/deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@gaubee/util",
3-
"version": "0.22.0",
3+
"version": "0.23.0",
44
"exports": {
55
"./abort": "./src/abort.ts",
66
"./bigint": "./src/bigint.ts",
@@ -23,6 +23,7 @@
2323
"./pure_event": "./src/pure_event.ts",
2424
"./readable_stream": "./src/readable_stream.ts",
2525
"./string": "./src/string.ts",
26+
"./throttle": "./src/throttle.ts",
2627
".": "./src/index.ts"
2728
},
2829
"publish": {

util/src/debounce.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Deno.test("should clear the timer with `cancel` method", async () => {
4747
const debounced = func_debounce(fn, 100);
4848

4949
debounced();
50-
debounced.clear();
50+
debounced.cancel();
5151

5252
await delay(200);
5353

util/src/debounce.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { type Timmer, timmers } from "./promise.ts";
55
export namespace func_debounce {
66
export type DebouncedFunction<F extends Func> = Func.SetReturn<F, Promise<Func.Return<F>>> & {
77
readonly isPending: boolean;
8-
clear(): void;
8+
cancel(): void;
99
source: F;
1010
flush(): void;
1111
};
@@ -64,7 +64,7 @@ export const func_debounce = <T extends Func>(
6464
get isPending() {
6565
return clear != null;
6666
},
67-
clear() {
67+
cancel() {
6868
clear?.();
6969
},
7070
source: fn,

util/src/func.ts

Lines changed: 73 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -174,37 +174,80 @@ export const extendsGetter = <T extends object>(
174174
};
175175

176176
export interface FuncCatch {
177-
<E = unknown, F extends Func = Func>(fn: F, errorParser?: (err: unknown) => E): FuncCatchWrapper<E, F>;
177+
<E = unknown, F extends Func = Func>(fn: F, errorParser?: (err: unknown) => E): FuncCatch.Wrapper<E, F>;
178+
wrapSuccess: <R>(resule: R) => FuncCatch.SuccessReturn<R>;
179+
wrapError: <E>(err: E, errorParser?: (err: unknown) => E) => FuncCatch.ErrorReturn<E>;
178180
}
179-
export type FuncCatchWrapper<E, F extends Func> =
180-
& Func<
181-
ThisParameterType<F>,
182-
Parameters<F>,
183-
FuncCatchReturn<E, ReturnType<F>>
184-
>
185-
& {
186-
catchType<E>(errorParser?: (err: unknown) => E): FuncCatchWrapper<E, F>;
181+
export namespace FuncCatch {
182+
export type Wrapper<E, F extends Func> =
183+
& Func<
184+
ThisParameterType<F>,
185+
Parameters<F>,
186+
Return<E, ReturnType<F>>
187+
>
188+
& {
189+
catchType<E>(errorParser?: (err: unknown) => E): Wrapper<E, F>;
190+
};
191+
export type SuccessReturn<R> = readonly [undefined, R] & {
192+
readonly success: true;
193+
readonly result: R;
194+
readonly error: void;
195+
};
196+
export type ErrorReturn<E> = readonly [E, undefined] & {
197+
readonly success: false;
198+
readonly result: void;
199+
readonly error: E;
187200
};
188-
export type FuncCatchReturn<E, R> = R extends PromiseLike<infer R> ? PromiseLike<[E, undefined] | [undefined, R]>
189-
: [E, undefined] | [undefined, R];
201+
export type Return<E, R> = [R] extends [never] ? ErrorReturn<E>
202+
: R extends PromiseLike<infer T> ? PromiseLike<SuccessReturn<T> | ErrorReturn<E>>
203+
: SuccessReturn<R> | ErrorReturn<E>;
204+
}
205+
206+
const wrapSuccess = <R>(result: R): FuncCatch.SuccessReturn<R> => {
207+
return Object.defineProperties(
208+
[undefined, result] as const,
209+
{
210+
success: { value: true, writable: false, enumerable: false, configurable: true },
211+
result: { value: result, writable: false, enumerable: false, configurable: true },
212+
error: { value: undefined, writable: false, enumerable: false, configurable: true },
213+
} as const,
214+
) as FuncCatch.SuccessReturn<R>;
215+
};
216+
const wrapError = <E>(err: E, errorParser?: (err: unknown) => E): FuncCatch.ErrorReturn<E> => {
217+
const error = errorParser ? errorParser(err) : err as E;
218+
return Object.defineProperties(
219+
[error, undefined] as const,
220+
{
221+
success: { value: false, writable: false, enumerable: false, configurable: true },
222+
result: { value: undefined, writable: false, enumerable: false, configurable: true },
223+
error: { value: error, writable: false, enumerable: false, configurable: true },
224+
} as const,
225+
) as FuncCatch.ErrorReturn<E>;
226+
};
190227
/** 包裹一个函数,并对其进行错误捕捉并返回 */
191-
export const func_catch: FuncCatch = <E = unknown, F extends Func = Func>(fn: F, errorParser?: (err: unknown) => E) => {
192-
return Object.assign(function (this: ThisParameterType<F>) {
193-
try {
194-
const res: ReturnType<F> = fn.apply(this, arguments as any);
195-
if (isPromiseLike(res)) {
196-
return res.then(
197-
(value: unknown) => [undefined, value],
198-
(err: unknown) => [errorParser ? errorParser(err) : err as E, undefined],
199-
);
228+
export const func_catch: FuncCatch = Object.assign(
229+
<E = unknown, F extends Func = Func>(fn: F, errorParser?: (err: unknown) => E) => {
230+
return Object.assign(function (this: ThisParameterType<F>) {
231+
try {
232+
const res: ReturnType<F> = fn.apply(this, arguments as any);
233+
if (isPromiseLike(res)) {
234+
return res.then(
235+
(value: unknown) => wrapSuccess(value),
236+
(err: unknown) => wrapError(err, errorParser),
237+
);
238+
}
239+
return wrapSuccess(res);
240+
} catch (err) {
241+
return wrapError(err, errorParser);
200242
}
201-
return [undefined, res];
202-
} catch (err) {
203-
return [errorParser ? errorParser(err) : err as E, undefined];
204-
}
205-
}, {
206-
catchType<E>(errorParser?: (err: unknown) => E) {
207-
return func_catch(fn, errorParser);
208-
},
209-
}) as FuncCatchWrapper<E, F>;
210-
};
243+
}, {
244+
catchType<E>(errorParser?: (err: unknown) => E) {
245+
return func_catch(fn, errorParser);
246+
},
247+
});
248+
},
249+
{
250+
wrapSuccess,
251+
wrapError,
252+
},
253+
);

util/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ export * from "./promise.ts";
2121
export * from "./pure_event.ts";
2222
export * from "./readable_stream.ts";
2323
export * from "./string.ts";
24+
export * from "./throttle.ts";

util/src/throttle.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { func_throttle } from "./throttle.ts";
2+
import assert from "node:assert";
3+
import { delay } from "./promise.ts";
4+
5+
Deno.test("should throttle function calls", async () => {
6+
let callCount = 0;
7+
const fn = () => callCount++;
8+
9+
// 测试 leading: false 的情况
10+
const throttled = func_throttle(fn, 100);
11+
12+
throttled(); // 不会立即执行
13+
assert.equal(callCount, 0);
14+
15+
await delay(150);
16+
assert.equal(callCount, 1); // 延迟后执行
17+
});
18+
Deno.test("should support `before` option", async () => {
19+
let callCount = 0;
20+
const fn = () => {
21+
callCount++;
22+
};
23+
const throttled = func_throttle(fn, 100, { before: true });
24+
25+
throttled(); // 立即执行(假设默认配置)
26+
assert.equal(callCount, 1); // 第一次立即调用
27+
28+
throttled();
29+
throttled();
30+
31+
assert.equal(callCount, 1);
32+
33+
await delay(150); // 超过节流时间
34+
assert.equal(callCount, 2); // 新时间窗口的第一次调用
35+
});
36+
37+
Deno.test("should cancel pending execution", async () => {
38+
let callCount = 0;
39+
const fn = () => callCount++;
40+
const throttled = func_throttle(fn, 100);
41+
42+
throttled(); // 立即执行
43+
throttled.cancel();
44+
45+
await delay(150);
46+
assert.equal(callCount, 0); // 取消后不应再执行
47+
});
48+
49+
Deno.test("should flush pending execution", async () => {
50+
let callCount = 0;
51+
const fn = () => callCount++;
52+
const throttled = func_throttle(fn, 100, { before: false });
53+
54+
throttled();
55+
throttled.flush(); // 立即执行
56+
57+
assert.equal(callCount, 1);
58+
await delay(150);
59+
assert.equal(callCount, 1); // 不应重复执行
60+
});
61+
62+
Deno.test("should return promise with result", async () => {
63+
const fn = () => "result";
64+
const throttled = func_throttle(fn, 100);
65+
66+
const result = await throttled();
67+
assert.equal(result, "result");
68+
});
69+
70+
Deno.test("should track pending status", async () => {
71+
const fn = () => {};
72+
const throttled = func_throttle(fn, 100, { before: false });
73+
74+
assert(!throttled.isPending);
75+
76+
throttled();
77+
assert(throttled.isPending); // 有等待中的执行
78+
79+
await delay(150);
80+
assert(!throttled.isPending);
81+
});

util/src/throttle.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { type Func, func_catch } from "./func.ts";
2+
import { obj_assign_props } from "./object.ts";
3+
import { type Timmer, timmers } from "./promise.ts";
4+
export namespace func_throttle {
5+
export type ThrottledFunction<F extends Func> = Func.SetReturn<F, Promise<Func.Return<F>>> & {
6+
readonly isPending: boolean;
7+
clear(): void;
8+
source: F;
9+
flush(): void;
10+
};
11+
}
12+
13+
export const func_throttle = <T extends Func>(
14+
fn: T,
15+
wait: number | Timmer = 0,
16+
options: { waitPromise?: boolean; before?: boolean } = {},
17+
) => {
18+
const timmer = timmers.from(wait);
19+
let clear: Timmer.Clear | undefined;
20+
21+
type R = Func.Return<T>;
22+
let jobs: PromiseWithResolvers<R>[] = [];
23+
24+
let target: Func | undefined;
25+
26+
return obj_assign_props(function throttled(this: Func.This<T>, ...args: Func.Args<T>) { // eslint-disable-line func-names
27+
const job = Promise.withResolvers<R>();
28+
if (clear == null) {
29+
clear = timmer(
30+
target = async () => {
31+
target = undefined;
32+
if (!options.waitPromise) {
33+
clear = undefined;
34+
}
35+
36+
const res = await func_catch(() => fn.apply(this, args) as Func.Return<T>)();
37+
38+
if (options.waitPromise) {
39+
clear = undefined;
40+
}
41+
if (res.success) {
42+
for (const job of jobs) {
43+
job.resolve(res.result);
44+
}
45+
} else {
46+
for (const job of jobs) {
47+
job.reject(res.error);
48+
}
49+
}
50+
jobs = [];
51+
},
52+
);
53+
if (options.before) {
54+
(async () => {
55+
const res = await func_catch(() => fn.apply(this, args) as Func.Return<T>)();
56+
if (res.success) {
57+
job.resolve(res.result);
58+
} else {
59+
job.reject(res.error);
60+
}
61+
})();
62+
} else {
63+
jobs.push(job);
64+
}
65+
} else {
66+
jobs.push(job);
67+
}
68+
69+
return job.promise;
70+
}, {
71+
get isPending() {
72+
return clear != null;
73+
},
74+
cancel() {
75+
clear?.();
76+
clear = undefined;
77+
},
78+
source: fn,
79+
flush() {
80+
clear?.();
81+
clear = undefined;
82+
target?.();
83+
},
84+
});
85+
};

0 commit comments

Comments
 (0)