feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
32
frontend/packages/common/chat-area/utils/src/async.ts
Normal file
32
frontend/packages/common/chat-area/utils/src/async.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const sleep = (t = 0) => new Promise(resolve => setTimeout(resolve, t));
|
||||
|
||||
export class Deferred<T = void> {
|
||||
promise: Promise<T>;
|
||||
resolve!: (value: T) => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- .
|
||||
reject!: (reason?: any) => void;
|
||||
then: Promise<T>['then'];
|
||||
constructor() {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
this.then = this.promise.then.bind(this.promise);
|
||||
}
|
||||
}
|
||||
33
frontend/packages/common/chat-area/utils/src/collection.ts
Normal file
33
frontend/packages/common/chat-area/utils/src/collection.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { safeAsyncThrow } from './safe-async-throw';
|
||||
|
||||
export const flatMapByKeyList = <T>(
|
||||
map: Map<string, T>,
|
||||
arr: string[],
|
||||
): T[] => {
|
||||
const res: T[] = [];
|
||||
for (const key of arr) {
|
||||
const val = map.get(key);
|
||||
if (!val) {
|
||||
safeAsyncThrow(`[flatMapByKeyList] cannot find ${key} in map`);
|
||||
continue;
|
||||
}
|
||||
res.push(val);
|
||||
}
|
||||
return res;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 检查没有遗漏的项
|
||||
*/
|
||||
export const exhaustiveCheckForRecord = (_: Record<string, never>) => undefined;
|
||||
|
||||
export const exhaustiveCheckSimple = (_: never) => undefined;
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isObject } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* @param inputError 传啥都行,一般是 catch (e) 那个 e
|
||||
* @param reason 多余的解释,我感觉有 eventName 了没啥用
|
||||
*/
|
||||
export const getReportError = (
|
||||
inputError: unknown,
|
||||
reason?: string,
|
||||
): {
|
||||
error: Error;
|
||||
meta: Record<string, unknown>;
|
||||
} => {
|
||||
if (inputError instanceof Error) {
|
||||
return {
|
||||
error: inputError,
|
||||
meta: { reason },
|
||||
};
|
||||
}
|
||||
if (!isObject(inputError)) {
|
||||
return {
|
||||
error: new Error(String(inputError)),
|
||||
meta: { reason },
|
||||
};
|
||||
}
|
||||
return {
|
||||
error: new Error(''),
|
||||
meta: { ...covertInputObject(inputError), reason },
|
||||
};
|
||||
};
|
||||
|
||||
const covertInputObject = (inputError: object) => {
|
||||
if ('reason' in inputError) {
|
||||
return {
|
||||
...inputError,
|
||||
reasonOfInputError: inputError.reason,
|
||||
};
|
||||
}
|
||||
return inputError;
|
||||
};
|
||||
70
frontend/packages/common/chat-area/utils/src/int64.ts
Normal file
70
frontend/packages/common/chat-area/utils/src/int64.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import bigInt, { max, min } from 'big-integer';
|
||||
|
||||
export const sortInt64CompareFn = (a: string, b: string) =>
|
||||
bigInt(a).compare(b);
|
||||
|
||||
/** O(1) 遍历 */
|
||||
export const getMinMax = (...nums: string[]) => {
|
||||
const num = nums.at(0);
|
||||
if (num === undefined) {
|
||||
return null;
|
||||
}
|
||||
let minRes = bigInt(num);
|
||||
let maxRes = bigInt(num);
|
||||
for (const curStr of nums) {
|
||||
const cur = bigInt(curStr);
|
||||
minRes = min(minRes, cur);
|
||||
maxRes = max(maxRes, cur);
|
||||
}
|
||||
return {
|
||||
min: minRes.toString(),
|
||||
max: maxRes.toString(),
|
||||
};
|
||||
};
|
||||
|
||||
export const getIsDiffWithinRange = (a: string, b: string, range: number) => {
|
||||
const diff = bigInt(a).minus(bigInt(b));
|
||||
const abs = diff.abs();
|
||||
return abs.lesser(bigInt(range));
|
||||
};
|
||||
|
||||
export const getInt64AbsDifference = (a: string, b: string) => {
|
||||
const diff = bigInt(a).minus(bigInt(b));
|
||||
const abs = diff.abs();
|
||||
return abs.toJSNumber();
|
||||
};
|
||||
|
||||
export const compareInt64 = (a: string) => {
|
||||
const bigA = bigInt(a);
|
||||
return {
|
||||
greaterThan: (b: string) => bigA.greater(bigInt(b)),
|
||||
lesserThan: (b: string) => bigA.lesser(bigInt(b)),
|
||||
eq: (b: string) => bigA.eq(bigInt(b)),
|
||||
};
|
||||
};
|
||||
|
||||
export const compute = (a: string) => {
|
||||
const bigA = bigInt(a);
|
||||
return {
|
||||
add: (b: string) => bigA.add(b).toString(),
|
||||
subtract: (b: string) => bigA.subtract(b).toString(),
|
||||
prev: () => bigA.prev().toString(),
|
||||
next: () => bigA.next().toString(),
|
||||
};
|
||||
};
|
||||
66
frontend/packages/common/chat-area/utils/src/json-parse.ts
Normal file
66
frontend/packages/common/chat-area/utils/src/json-parse.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const typeSafeJsonParse = (
|
||||
str: string,
|
||||
onParseError: (error: Error) => void,
|
||||
): unknown => {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
onParseError(e as Error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 泛型类型标注可能需要使用 type 声明,
|
||||
* refer: https://github.com/microsoft/TypeScript/issues/15300.
|
||||
*/
|
||||
export const typeSafeJsonParseEnhanced = <T>({
|
||||
str,
|
||||
onParseError,
|
||||
verifyStruct,
|
||||
onVerifyError,
|
||||
}: {
|
||||
str: string;
|
||||
onParseError: (error: Error) => void;
|
||||
/**
|
||||
* 实现一个类型校验,返回是否通过(boolean);实际上还是靠自觉.
|
||||
* 可以单独定义, 也可以写作内联 function, 但是注意返回值标注为 predicate,
|
||||
* refer: https://github.com/microsoft/TypeScript/issues/38390.
|
||||
*/
|
||||
verifyStruct: (sth: unknown) => sth is T;
|
||||
/** 错误原因: 校验崩溃; 校验未通过 */
|
||||
onVerifyError: (error: Error) => void;
|
||||
}): T | null => {
|
||||
const res = typeSafeJsonParse(str, onParseError);
|
||||
|
||||
function assertStruct(resLocal: unknown): asserts resLocal is T {
|
||||
const ok = verifyStruct(resLocal);
|
||||
if (!ok) {
|
||||
throw new Error('verify struct no pass');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
assertStruct(res);
|
||||
return res;
|
||||
} catch (e) {
|
||||
onVerifyError(e as Error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Text, type Link, type Parent, type Image } from 'mdast';
|
||||
import { isObject, isUndefined } from 'lodash-es';
|
||||
/**
|
||||
* 将markdown转为纯文本
|
||||
* @param markdown Markdown文本
|
||||
* @returns string 纯文本
|
||||
*/
|
||||
export const getTextFromAst = (ast: unknown): string => {
|
||||
if (isParent(ast)) {
|
||||
return `${ast.children.map(child => getTextFromAst(child)).join('')}`;
|
||||
}
|
||||
|
||||
if (isText(ast)) {
|
||||
return ast.value;
|
||||
}
|
||||
|
||||
if (isLink(ast)) {
|
||||
return `[${getTextFromAst(ast.children)}](${ast.url})`;
|
||||
}
|
||||
|
||||
if (isImage(ast)) {
|
||||
return ``;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const isParent = (ast: unknown): ast is Parent =>
|
||||
!!ast && isObject(ast) && 'children' in ast && !isUndefined(ast?.children);
|
||||
|
||||
const isLink = (ast: unknown): ast is Link =>
|
||||
isObject(ast) && 'type' in ast && !isUndefined(ast) && ast.type === 'link';
|
||||
|
||||
const isImage = (ast: unknown): ast is Image =>
|
||||
!isUndefined(ast) && isObject(ast) && 'type' in ast && ast.type === 'image';
|
||||
|
||||
const isText = (ast: unknown): ast is Text =>
|
||||
!isUndefined(ast) && isObject(ast) && 'type' in ast && ast.type === 'text';
|
||||
|
||||
export const parseMarkdownHelper = {
|
||||
isParent,
|
||||
isLink,
|
||||
isImage,
|
||||
isText,
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isNumber, isObject, isString } from 'lodash-es';
|
||||
|
||||
type CheckMethodName = 'is-string' | 'is-number';
|
||||
|
||||
const checkMethodsMap = new Map<CheckMethodName, (sth: unknown) => boolean>([
|
||||
['is-string', isString],
|
||||
['is-number', isNumber],
|
||||
]);
|
||||
|
||||
/**
|
||||
* think about:
|
||||
* https://www.npmjs.com/package/type-plus
|
||||
* https://www.npmjs.com/package/generic-type-guard
|
||||
* https://github.com/runtypes/runtypes
|
||||
*/
|
||||
export const performSimpleObjectTypeCheck = <T extends Record<string, unknown>>(
|
||||
sth: unknown,
|
||||
pairs: [key: keyof T, checkMethod: CheckMethodName][],
|
||||
): sth is T => {
|
||||
if (!isObject(sth)) {
|
||||
return false;
|
||||
}
|
||||
return pairs.every(([k, type]) => {
|
||||
if (!(k in sth)) {
|
||||
return false;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- runtime safe
|
||||
// @ts-expect-error
|
||||
const val = sth[k];
|
||||
return checkMethodsMap.get(type)?.(val);
|
||||
});
|
||||
};
|
||||
86
frontend/packages/common/chat-area/utils/src/rate-limit.ts
Normal file
86
frontend/packages/common/chat-area/utils/src/rate-limit.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { sleep } from './async';
|
||||
|
||||
type Fn<ARGS extends unknown[], Ret = unknown> = (...args: ARGS) => Ret;
|
||||
|
||||
/**
|
||||
* 限流器,对于被限流的异步方法进行以下形式的限流:
|
||||
* 1. 在 timeWindow 内的前 limit 个请求不做限制,立即发送
|
||||
* 2. timeWindow 内超过 limit 个请求后,对每个请求依次添加 onLimitDelay 毫秒的延迟
|
||||
*
|
||||
* 注意是排队添加,形如 invoke: [1(0ms), 2(0ms), 3(0ms), 4(0ms)]; limit: [1(0ms), 2(0ms), 3(100ms), 4(200ms)]
|
||||
*
|
||||
* 另注:这个设计遭到了猛烈抨击,认为 debounce 可以代替掉,实现过于复杂,但是考虑:
|
||||
* 1. 支持列表双向加载的拉取,简单使用 debounce 可能导致请求某侧丢失;添加延时可以保证不丢失请求
|
||||
* 2. 列表拉取一旦出现死循环,可能导致恶性问题,如密集地对服务端接口的高频访问
|
||||
*
|
||||
* 以上场景通常不应出现,所以 limit 设计也只是对极端场景的兜底,上层 UI 错误理应得到妥善解决
|
||||
* TODO: wlt - 补充 testcase
|
||||
*/
|
||||
export class RateLimit<ARGS extends unknown[], Ret> {
|
||||
constructor(
|
||||
private fn: Fn<ARGS, Promise<Ret>>,
|
||||
private config: {
|
||||
onLimitDelay: number;
|
||||
limit: number;
|
||||
timeWindow: number;
|
||||
},
|
||||
) {}
|
||||
|
||||
private records: number[] = [];
|
||||
|
||||
private getNewInvokeDelay(): number {
|
||||
const { timeWindow, limit, onLimitDelay } = this.config;
|
||||
const now = Date.now();
|
||||
const windowEdge = now - timeWindow;
|
||||
const idx = this.records.findIndex(t => t >= windowEdge);
|
||||
if (idx < 0) {
|
||||
return 0;
|
||||
}
|
||||
const lasts = this.records.slice(idx);
|
||||
if (lasts.length < limit) {
|
||||
return 0;
|
||||
}
|
||||
const last = lasts.at(-1);
|
||||
if (!last) {
|
||||
return 0;
|
||||
}
|
||||
return last + onLimitDelay - now;
|
||||
}
|
||||
|
||||
private clearRecords() {
|
||||
const { timeWindow } = this.config;
|
||||
const now = Date.now();
|
||||
const windowEdge = now - timeWindow;
|
||||
const idx = this.records.findLastIndex(t => t < windowEdge);
|
||||
if (idx >= 0) {
|
||||
this.records = this.records.slice(idx + 1);
|
||||
}
|
||||
}
|
||||
|
||||
invoke = async (...args: ARGS): Promise<Ret> => {
|
||||
const invokeDelay = this.getNewInvokeDelay();
|
||||
const now = Date.now();
|
||||
this.records.push(invokeDelay + now);
|
||||
if (invokeDelay) {
|
||||
await sleep(invokeDelay);
|
||||
}
|
||||
this.clearRecords();
|
||||
return this.fn(...args);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 非线上环境阻塞;构建后仅做异常输出和异步抛出错误
|
||||
*/
|
||||
export const safeAsyncThrow = (e: string) => {
|
||||
const err = new Error(`[chat-area] ${e}`);
|
||||
if (IS_DEV_MODE || IS_BOE) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
20
frontend/packages/common/chat-area/utils/src/type-helper.ts
Normal file
20
frontend/packages/common/chat-area/utils/src/type-helper.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 不知道为啥 unknown 不行,会导致类型转换失败
|
||||
export type MakeValueUndefinable<T extends Record<string, any>> = {
|
||||
[k in keyof T]: T[k] | undefined;
|
||||
};
|
||||
17
frontend/packages/common/chat-area/utils/src/typings.d.ts
vendored
Normal file
17
frontend/packages/common/chat-area/utils/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isUndefined, omitBy } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* zustand update 辅助方法,检查入参对象,丢弃 value 为 undefined 的项.
|
||||
* zustand 自身没有过滤逻辑,如果类型没有问题,可能意外地将项目置为 undefined 值
|
||||
*/
|
||||
export const updateOnlyDefined = <T extends Record<string, unknown>>(
|
||||
updater: (sth: T) => void,
|
||||
val: T,
|
||||
) => {
|
||||
const left = omitBy(val, isUndefined) as T;
|
||||
if (!Object.keys(left).length) {
|
||||
return;
|
||||
}
|
||||
updater(left);
|
||||
};
|
||||
Reference in New Issue
Block a user