@taicode/common-base 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ export function debounce(wait: number) {
2
+ function debounce<T extends (this: any, ...args: any[]) => Promise<any>>(originalMethod: T, context: ClassMethodDecoratorContext): T {
3
+ let timeout: unknown = null
4
+
5
+ async function replacementMethod(this: any, ...args: any[]): Promise<any> {
6
+ return new Promise((resolve, reject) => {
7
+ if (timeout) clearTimeout(timeout as any)
8
+ timeout = setTimeout(() => {
9
+ originalMethod.call(this, ...args)
10
+ .then(resolve)
11
+ .catch(reject)
12
+ }, wait)
13
+ })
14
+ }
15
+
16
+ context.addInitializer(function () {
17
+ const self = this as any
18
+ self[context.name] = replacementMethod.bind(this)
19
+ })
20
+
21
+ return replacementMethod as T
22
+ }
23
+
24
+ return debounce
25
+ }
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@taicode/common-base",
3
+ "version": "1.0.0",
4
+ "author": "Alain",
5
+ "license": "ISC",
6
+ "description": "",
7
+ "exports": {
8
+ "./utils/*": ["./source/utils/*"],
9
+ "./descriptors/*": ["./source/descriptors/*"]
10
+ }
11
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,103 @@
1
+ {
2
+ "compilerOptions": {
3
+ /* Visit https://aka.ms/tsconfig to read more about this file */
4
+
5
+ /* Projects */
6
+ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7
+ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8
+ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9
+ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10
+ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11
+ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12
+
13
+ /* Language and Environment */
14
+ "target": "es2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15
+ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16
+ // "jsx": "preserve", /* Specify what JSX code is generated. */
17
+ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18
+ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19
+ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20
+ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21
+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22
+ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23
+ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24
+ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25
+ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26
+
27
+ /* Modules */
28
+ "module": "ESNext", /* Specify what module code is generated. */
29
+ // "rootDir": "./", /* Specify the root folder within your source files. */
30
+ "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
31
+ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32
+ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33
+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34
+ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35
+ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37
+ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38
+ // "resolveJsonModule": true, /* Enable importing .json files. */
39
+ // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
40
+
41
+ /* JavaScript Support */
42
+ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
43
+ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
44
+ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
45
+
46
+ /* Emit */
47
+ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48
+ "declarationMap": true, /* Create sourcemaps for d.ts files. */
49
+ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50
+ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51
+ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
52
+ // "outDir": "./", /* Specify an output folder for all emitted files. */
53
+ // "removeComments": true, /* Disable emitting comments. */
54
+ // "noEmit": true, /* Disable emitting files from a compilation. */
55
+ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
56
+ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
57
+ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
58
+ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
59
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60
+ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
61
+ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62
+ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63
+ // "newLine": "crlf", /* Set the newline character for emitting files. */
64
+ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
65
+ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
66
+ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67
+ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
68
+ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
69
+ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70
+
71
+ /* Interop Constraints */
72
+ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
73
+ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74
+ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
75
+ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76
+ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
77
+
78
+ /* Type Checking */
79
+ "strict": true, /* Enable all strict type-checking options. */
80
+ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
81
+ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
82
+ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
83
+ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
84
+ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
85
+ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
86
+ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
87
+ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
88
+ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
89
+ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90
+ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
91
+ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
92
+ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
93
+ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
94
+ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
95
+ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
96
+ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
97
+ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
98
+
99
+ /* Completeness */
100
+ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
101
+ "skipLibCheck": true /* Skip type checking all .d.ts files. */
102
+ }
103
+ }
@@ -0,0 +1,66 @@
1
+ type SeedValue = number | string | null
2
+
3
+ function normalizeSeed(value: SeedValue): number {
4
+ if (typeof value === 'number') {
5
+ return value
6
+ }
7
+
8
+ if (typeof value === 'string') {
9
+ return value.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0)
10
+ }
11
+
12
+ return 0
13
+ }
14
+
15
+ export function getRandomColor(seed: SeedValue) {
16
+ // 使用 asciiSum 生成 HSL 值
17
+ const hue = normalizeSeed(seed) % 360 // 色相(0-360)
18
+ const saturation = 80 // 饱和度(0-100)
19
+ const lightness = 45 // 明度(0-100)
20
+
21
+ return (offset: SeedValue, range: number = 360) => {
22
+ const normalizedOffset = normalizeSeed(offset)
23
+ console.log('normalizedOffset', offset, normalizedOffset)
24
+
25
+ const finalHue = (hue + normalizedOffset) % range
26
+ return `hsl(${finalHue}, ${saturation}%, ${lightness}%)`
27
+ }
28
+ }
29
+
30
+ export function getRandomColors(seed: SeedValue, interval: number, count: number) {
31
+ const colors = []
32
+ const colorFunc = getRandomColor(seed)
33
+ for (let i = 0; i < count; i++) {
34
+ colors.push(colorFunc(i * interval))
35
+ }
36
+ return colors
37
+ }
38
+
39
+ export function interpolateColors(color1: string, color2: string, steps: number) {
40
+ function parseHSL(color: string): [number, number, number] {
41
+ const match = color.match(/hsl\((\d+),\s*(\d+)%?,\s*(\d+)%?\)/)
42
+ if (!match) {
43
+ throw new Error(`Invalid HSL color: ${color}`)
44
+ }
45
+ return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])]
46
+ }
47
+
48
+ function hslToString(h: number, s: number, l: number): string {
49
+ return `hsl(${Math.round(h)}, ${Math.round(s)}%, ${Math.round(l)}%)`
50
+ }
51
+
52
+ const [h1, s1, l1] = parseHSL(color1)
53
+ const [h2, s2, l2] = parseHSL(color2)
54
+
55
+ return (index: number): string => {
56
+ if (index < 0 || index > steps) {
57
+ throw new Error(`Index out of bounds: ${index}. Valid range is 0 to ${steps}.`)
58
+ }
59
+
60
+ const ratio = index / steps
61
+ const h = h1 + (h2 - h1) * ratio
62
+ const s = s1 + (s2 - s1) * ratio
63
+ const l = l1 + (l2 - l1) * ratio
64
+ return hslToString(h, s, l)
65
+ }
66
+ }
@@ -0,0 +1,20 @@
1
+ export class Disposer {
2
+ private cleanupFunctions: (() => unknown)[] = [];
3
+
4
+ // 添加清理函数
5
+ addDisposer(cleanupFn: () => unknown): void {
6
+ this.cleanupFunctions.push(cleanupFn)
7
+ }
8
+
9
+ // 执行所有清理函数
10
+ dispose(): void {
11
+ for (const cleanupFn of this.cleanupFunctions) {
12
+ try {
13
+ cleanupFn()
14
+ } catch (error) {
15
+ console.error('Error during cleanup:', error)
16
+ }
17
+ }
18
+ this.cleanupFunctions = [] // 清空清理函数列表
19
+ }
20
+ }
@@ -0,0 +1,77 @@
1
+ type OffFn = () => void
2
+ type Listener<T = unknown> = (data: T) => void
3
+
4
+ export class EventEmitter<Events extends object = object> {
5
+ private listeners: { [K in keyof Events]?: Listener<Events[K]>[] } = {}
6
+
7
+ constructor() {
8
+ this.on = this.on.bind(this)
9
+ this.off = this.off.bind(this)
10
+ this.once = this.once.bind(this)
11
+ this.emit = this.emit.bind(this)
12
+ this.cleanup = this.cleanup.bind(this)
13
+ }
14
+
15
+ /**
16
+ * 订阅事件
17
+ * @param type 事件类型
18
+ * @param listener 回调函数
19
+ */
20
+ on<K extends keyof Events>(type: K, listener: Listener<Events[K]>): OffFn {
21
+ if (!this.listeners[type]) {
22
+ this.listeners[type] = []
23
+ }
24
+ this.listeners[type]!.push(listener)
25
+
26
+ return () => this.off(type, listener)
27
+ }
28
+
29
+ /**
30
+ * 取消订阅
31
+ * @param type 事件类型
32
+ * @param listener 要移除的回调函数(可选)
33
+ */
34
+ off<K extends keyof Events>(type: K, listener?: Listener<Events[K]>): void {
35
+ if (!this.listeners[type]) return
36
+
37
+ if (!listener) {
38
+ // 移除该事件的所有监听器
39
+ delete this.listeners[type]
40
+ } else {
41
+ // 移除特定监听器
42
+ this.listeners[type] = this.listeners[type]!.filter(
43
+ (fn) => fn !== listener
44
+ )
45
+ }
46
+ }
47
+
48
+ /**
49
+ * 触发事件
50
+ * @param type 事件类型
51
+ * @param data 要传递的数据
52
+ */
53
+ emit<K extends keyof Events>(type: K, data: Events[K]): void {
54
+ const listeners = this.listeners[type]
55
+ // 复制数组避免回调中取消订阅导致的遍历问题
56
+ if (listeners) {
57
+ listeners.slice().forEach((fn) => fn(data))
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 一次性订阅
63
+ * @param type 事件类型
64
+ * @param listener 回调函数
65
+ */
66
+ once<K extends keyof Events>(type: K, listener: Listener<Events[K]>): void {
67
+ const onceWrapper: Listener<Events[K]> = (data) => {
68
+ listener(data)
69
+ this.off(type, onceWrapper)
70
+ }
71
+ this.on(type, onceWrapper)
72
+ }
73
+
74
+ cleanup() {
75
+ this.listeners = {}
76
+ }
77
+ }
@@ -0,0 +1,25 @@
1
+ export function bigintToNumber(value: bigint | null): number {
2
+ if (value === null) return 0
3
+
4
+ // JavaScript 的 number 类型最大值
5
+ const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER // 2^53 - 1
6
+ const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER // -(2^53 - 1)
7
+
8
+ try {
9
+ // 检查输入是否为 bigint
10
+ if (typeof value !== 'bigint') {
11
+ throw new TypeError('Input must be a bigint')
12
+ }
13
+
14
+ // 检查是否溢出
15
+ if (value > BigInt(MAX_SAFE_INTEGER) || value < BigInt(MIN_SAFE_INTEGER)) {
16
+ return MAX_SAFE_INTEGER // 溢出,返回 NaN
17
+ }
18
+
19
+ // 将 bigint 转换为 number
20
+ return Number(value)
21
+ } catch {
22
+ // 捕获任何错误并返回 NaN
23
+ return 0
24
+ }
25
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { objectMerge } from './'
3
+
4
+ describe('merge function', () => {
5
+ it('should merge two simple objects', () => {
6
+ const target = { a: 1 } as object
7
+ const source = { b: 2 } as object
8
+ const result = objectMerge(target, source)
9
+
10
+ expect(result).toEqual({ a: 1, b: 2 })
11
+ })
12
+
13
+ it('should merge nested objects', () => {
14
+ const target = { a: 1, b: { c: 2 } } as object
15
+ const source = { b: { d: 3 }, e: 4 } as object
16
+ const result = objectMerge(target, source)
17
+
18
+ expect(result).toEqual({ a: 1, b: { c: 2, d: 3 }, e: 4 })
19
+ })
20
+
21
+ it('should merge multiple objects', () => {
22
+ const target = { a: 1 } as object
23
+ const source1 = { b: 2 } as object
24
+ const source2 = { c: 3 } as object
25
+ const result = objectMerge(target, source1, source2)
26
+
27
+ expect(result).toEqual({ a: 1, b: 2, c: 3 })
28
+ })
29
+
30
+ it('should overwrite existing properties', () => {
31
+ const target = { a: 1, b: 2 } as object
32
+ const source = { b: 3, c: 4 } as object
33
+ const result = objectMerge(target, source)
34
+
35
+ expect(result).toEqual({ a: 1, b: 3, c: 4 })
36
+ })
37
+
38
+ it('should handle null and array values', () => {
39
+ const target = { a: 1, b: null } as object
40
+ const source = { b: [1, 2, 3], c: 4 }
41
+ const result = objectMerge(target, source)
42
+
43
+ expect(result).toEqual({ a: 1, b: [1, 2, 3], c: 4 })
44
+ })
45
+
46
+ it('should not mutate the original target object', () => {
47
+ const target = { a: 1 } as object
48
+ const source = { b: 2 } as object
49
+ const result = objectMerge(target, source)
50
+
51
+ expect(result).toEqual({ a: 1, b: 2 })
52
+ expect(source).toEqual({ b: 2 }) // Target should remain unchanged
53
+ })
54
+ })
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 递归合并对象,只针对对象类型的属性进行合并,其他类型后者覆盖前者
3
+ */
4
+ export function objectMerge<T extends object>(target: T, ...rest: Partial<T>[]): T {
5
+ for (const source of rest) {
6
+ for (const key in source) {
7
+ if (source.hasOwnProperty(key)) {
8
+ if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
9
+ if (!target.hasOwnProperty(key)) {
10
+ (target as any)[key] = {}
11
+ }
12
+ objectMerge((target as any)[key], (source as any)[key])
13
+ } else {
14
+ (target as any)[key] = source[key]
15
+ }
16
+ }
17
+ }
18
+ }
19
+
20
+ return target
21
+ }
22
+
23
+ export function ObjectKeys<T extends object>(obj: T): (keyof T)[] {
24
+ return Object.keys(obj) as (keyof T)[]
25
+ }
26
+
27
+ export function ObjectValues<T extends object>(obj: T): T[keyof T][] {
28
+ return Object.values(obj) as T[keyof T][]
29
+ }
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach, MockedFunction } from 'vitest'
2
+ import { Poller } from './'
3
+
4
+ describe('Poller', () => {
5
+ let poller: Poller
6
+ let task: MockedFunction<() => Promise<void> | void>
7
+ const interval = 100 // 100ms 的间隔
8
+
9
+ beforeEach(() => {
10
+ task = vi.fn()
11
+ poller = new Poller(task, interval)
12
+ })
13
+
14
+ afterEach(() => {
15
+ poller.stop() // 确保每次测试后停止轮询
16
+ })
17
+
18
+ it('should start and stop the poller', async () => {
19
+ poller.start()
20
+ expect(poller['isRunning']).toBe(true)
21
+
22
+ poller.stop()
23
+ expect(poller['isRunning']).toBe(false)
24
+ })
25
+
26
+ it('should execute the task at the specified interval', async () => {
27
+ poller.start()
28
+ await new Promise((resolve) => setTimeout(resolve, interval * 2)) // 等待两个间隔
29
+
30
+ expect(task).toHaveBeenCalledTimes(2) // 任务应该被执行两次
31
+ })
32
+
33
+ it('should stop the poller and clear the timeout', async () => {
34
+ poller.start()
35
+ await new Promise((resolve) => setTimeout(resolve, interval / 2)) // 等待半个间隔
36
+ poller.stop()
37
+
38
+ expect(poller['timeoutId']).toBeUndefined() // 定时器应该被清除
39
+ expect(poller['resolveCurrentWait']).toBeUndefined() // resolve 函数应该被清除
40
+ })
41
+
42
+ it('should handle task errors and continue polling', async () => {
43
+ const error = new Error('Task failed')
44
+ task.mockRejectedValueOnce(error) // 模拟任务第一次执行时出错
45
+
46
+ const consoleSpy = vi.spyOn(console, 'error') // 捕获 console.error 调用
47
+
48
+ poller.start()
49
+ await new Promise((resolve) => setTimeout(resolve, interval * 2)) // 等待两个间隔
50
+
51
+ expect(consoleSpy).toHaveBeenCalledWith('Task execution error:', error) // 应该捕获到错误
52
+ expect(task).toHaveBeenCalledTimes(2) // 任务应该被执行两次,即使第一次出错
53
+ })
54
+
55
+ it('should stop the poller immediately when stop is called', async () => {
56
+ poller.start()
57
+ await new Promise((resolve) => setTimeout(resolve, interval / 2)) // 等待半个间隔
58
+ poller.stop()
59
+
60
+ expect(task).toHaveBeenCalledTimes(1) // 任务应该只被执行一次
61
+ })
62
+ })
@@ -0,0 +1,57 @@
1
+ export type PollerTask = () => Promise<void> | void
2
+
3
+ export class Poller {
4
+ private task: PollerTask // 需要执行的函数
5
+ private interval: number // 执行间隔(毫秒)
6
+ private isRunning: boolean = false; // 运行状态
7
+ private timeoutId?: number // 定时器 ID
8
+ private resolveCurrentWait?: () => void // 用于中断等待的 resolve 函数
9
+
10
+ constructor(task: PollerTask, interval: number) {
11
+ this.task = task
12
+ this.interval = interval
13
+ }
14
+
15
+ // 异步执行循环
16
+ private async run(): Promise<void> {
17
+ while (this.isRunning) {
18
+ try {
19
+ await this.task() // 执行任务(同步/异步)
20
+ } catch (error) {
21
+ console.error("Task execution error:", error) // 错误处理
22
+ }
23
+
24
+ // 等待指定间隔(可被 stop 中断)
25
+ await new Promise<void>((resolve) => {
26
+ this.resolveCurrentWait = resolve
27
+ this.timeoutId = setTimeout(() => {
28
+ this.resolveCurrentWait = undefined
29
+ resolve()
30
+ }, this.interval)
31
+ })
32
+ }
33
+ }
34
+
35
+ // 启动轮训
36
+ start(): void {
37
+ if (this.isRunning) return
38
+ this.isRunning = true
39
+ this.run()
40
+ }
41
+
42
+ // 停止轮训
43
+ stop(): void {
44
+ if (!this.isRunning) return
45
+ this.isRunning = false
46
+
47
+ // 清除定时器和立即结束等待
48
+ if (this.timeoutId) {
49
+ clearTimeout(this.timeoutId)
50
+ this.timeoutId = undefined
51
+ }
52
+ if (this.resolveCurrentWait) {
53
+ this.resolveCurrentWait()
54
+ this.resolveCurrentWait = undefined
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { catchE, catchP } from '.'
3
+
4
+ describe('@helper/catch test', () => {
5
+ it('test catchP', async () => {
6
+ const testCases = [
7
+ { input: Promise.reject(1), output: [undefined, 1] },
8
+ { input: Promise.resolve(1), output: [1, undefined] },
9
+ { input: Promise.reject({}), output: [undefined, {}] },
10
+ { input: Promise.resolve({}), output: [{}, undefined] },
11
+ { input: Promise.reject(null), output: [undefined, null] },
12
+ { input: Promise.resolve(null), output: [null, undefined] },
13
+ ]
14
+
15
+ for (let index = 0; index < testCases.length; index++) {
16
+ const testCase = testCases[index]
17
+ const result = await catchP(testCase.input)
18
+ expect(result).toEqual(testCase.output)
19
+ }
20
+ })
21
+
22
+ it('test catchE', async () => {
23
+ const testCases = [
24
+ { input: () => { throw 1 }, output: [undefined, 1] },
25
+ { input: () => { return 1 }, output: [1, undefined] },
26
+ { input: () => { throw {} }, output: [undefined, {}] },
27
+ { input: () => { return {} }, output: [{}, undefined] },
28
+ { input: () => { throw null }, output: [undefined, null] },
29
+ { input: () => { return null }, output: [null, undefined] },
30
+ ]
31
+
32
+ for (let index = 0; index < testCases.length; index++) {
33
+ const testCase = testCases[index]
34
+ const result = await catchE(testCase.input)
35
+ expect(result).toEqual(testCase.output)
36
+ }
37
+ })
38
+ })
@@ -0,0 +1,45 @@
1
+
2
+ /**
3
+ * 将 Promise<R> 转换成 [resolve:R, reject:unknown] 的形式
4
+ */
5
+ export async function catchP<R>(p: Promise<R>): Promise<[R, unknown]> {
6
+ const returned: [R, unknown] = [undefined, undefined] as unknown as [R, unknown]
7
+
8
+ await p
9
+ .then((result) => (returned[0] = result))
10
+ .catch((error: unknown) => (returned[1] = error))
11
+
12
+ return returned
13
+ }
14
+
15
+ type CatchType<F extends () => unknown> = F extends () => Promise<infer R> ? R : F extends () => infer R ? R : never
16
+
17
+ /**
18
+ * 执行函数 ()=>R 并以 [return: R, error:unknown] 的形式返回结果
19
+ */
20
+ export async function catchE<F extends () => unknown>(f: F): Promise<[CatchType<F>, unknown]> {
21
+ const returned: [CatchType<F>, unknown] = [undefined, undefined] as unknown as [CatchType<F>, unknown]
22
+
23
+ try {
24
+ const result = f()
25
+ if (result instanceof Promise) {
26
+ return catchP(result)
27
+ } else {
28
+ returned[0] = result as CatchType<F>
29
+ }
30
+ } catch (error) {
31
+ returned[1] = error
32
+ }
33
+
34
+ return returned
35
+ }
36
+
37
+ declare global {
38
+ interface Promise<T> {
39
+ result<R = T>(): Promise<[R, unknown]>
40
+ }
41
+ }
42
+
43
+ Promise.prototype.result = function () {
44
+ return catchP(this)
45
+ }
@@ -0,0 +1,9 @@
1
+ /** 生成随机字符串,可以指定字符集和长度 */
2
+ export function randomString(length: number, charset: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'): string {
3
+ let result = ''
4
+ const charsetLength = charset.length
5
+ for (let i = 0; i < length; i++) {
6
+ result += charset.charAt(Math.floor(Math.random() * charsetLength))
7
+ }
8
+ return result
9
+ }