@greypan/js-kit 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.
- package/.turbo/turbo-build.log +38 -0
- package/.turbo/turbo-test.log +87 -0
- package/README.CN.md +5 -0
- package/README.md +5 -0
- package/dist/_virtual/_rolldown/runtime.js +11 -0
- package/dist/fetch/compose.d.ts +3 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +10 -0
- package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/funnel.js +40 -0
- package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/funnel.js.map +1 -0
- package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/isNullish.js +8 -0
- package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/isNullish.js.map +1 -0
- package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/lazyDataLastImpl--3B10z3s.js +12 -0
- package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/lazyDataLastImpl--3B10z3s.js.map +1 -0
- package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/omitBy.js +14 -0
- package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/omitBy.js.map +1 -0
- package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/purry.js +12 -0
- package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/purry.js.map +1 -0
- package/dist/number/index.d.ts +14 -0
- package/dist/number/index.js +13 -0
- package/dist/number/index.js.map +1 -0
- package/dist/paradigm/go.d.ts +12 -0
- package/dist/paradigm/go.js +37 -0
- package/dist/paradigm/go.js.map +1 -0
- package/dist/paradigm/index.d.ts +2 -0
- package/dist/paradigm/rust.d.ts +25 -0
- package/dist/paradigm/rust.js +42 -0
- package/dist/paradigm/rust.js.map +1 -0
- package/dist/plugin/batching-emitter.d.ts +4 -0
- package/dist/plugin/batching-emitter.js +33 -0
- package/dist/plugin/batching-emitter.js.map +1 -0
- package/dist/plugin/core.d.ts +10 -0
- package/dist/plugin/core.js +23 -0
- package/dist/plugin/core.js.map +1 -0
- package/dist/plugin/event-emitter.d.ts +4 -0
- package/dist/plugin/index.d.ts +2 -0
- package/dist/random/index.d.ts +23 -0
- package/dist/random/index.js +18 -0
- package/dist/random/index.js.map +1 -0
- package/dist/timer/controllable-interval.d.ts +34 -0
- package/dist/timer/controllable-interval.js +41 -0
- package/dist/timer/controllable-interval.js.map +1 -0
- package/dist/timer/debounce.d.ts +21 -0
- package/dist/timer/debounce.js +30 -0
- package/dist/timer/debounce.js.map +1 -0
- package/dist/timer/index.d.ts +2 -0
- package/dist/url/index.d.ts +9 -0
- package/dist/url/index.js +28 -0
- package/dist/url/index.js.map +1 -0
- package/dist/utility-types/index.d.ts +13 -0
- package/package.json +54 -0
- package/src/fetch/__tests__/compose.spec.ts +105 -0
- package/src/fetch/compose.ts +23 -0
- package/src/index.ts +7 -0
- package/src/number/__tests__/number.spec.ts +50 -0
- package/src/number/index.ts +22 -0
- package/src/paradigm/__tests__/go.spec.ts +63 -0
- package/src/paradigm/__tests__/rust.spec.ts +68 -0
- package/src/paradigm/go.ts +38 -0
- package/src/paradigm/index.ts +2 -0
- package/src/paradigm/rust.ts +50 -0
- package/src/plugin/__tests__/batch-emitter.spec.ts +48 -0
- package/src/plugin/__tests__/core.spec.ts +110 -0
- package/src/plugin/__tests__/event-emiiter.spec.ts +41 -0
- package/src/plugin/batching-emitter.ts +61 -0
- package/src/plugin/core.ts +37 -0
- package/src/plugin/event-emitter.ts +25 -0
- package/src/plugin/index.ts +2 -0
- package/src/random/__tests__/random.spec.ts +77 -0
- package/src/random/index.ts +37 -0
- package/src/timer/__tests__/controllable-interval.spec.ts +51 -0
- package/src/timer/controllable-interval.ts +86 -0
- package/src/timer/debounce.ts +92 -0
- package/src/timer/index.ts +2 -0
- package/src/url/README.md +0 -0
- package/src/url/__tests__/url.spec.ts +152 -0
- package/src/url/index.ts +44 -0
- package/src/utility-types/index.ts +19 -0
- package/tsconfig.app.json +11 -0
- package/tsconfig.json +8 -0
- package/tsconfig.node.json +18 -0
- package/tsconfig.vitest.json +10 -0
- package/vite.config.ts +32 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Rust 范式,用于统一处理成功/失败返回值
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type Ok<T> = {
|
|
6
|
+
readonly ok: true
|
|
7
|
+
readonly value: T
|
|
8
|
+
}
|
|
9
|
+
export type Err<E> = {
|
|
10
|
+
readonly ok: false
|
|
11
|
+
readonly error: E
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 考虑到 T 或 E 其中一个为 never 的情况
|
|
15
|
+
* Result<T,never> 等价于 Ok<T>
|
|
16
|
+
* Result<never,E> 等价于 Err<E>
|
|
17
|
+
* 为什么是 [T] extends [never] ? 而不是 T extends never ?:
|
|
18
|
+
* 条件类型是分布式的,防止 T 为联合类型时对每项分别计算再合并结果
|
|
19
|
+
*/
|
|
20
|
+
export type Result<T, E> = Ok<T> | Err<E>
|
|
21
|
+
|
|
22
|
+
export function ok<T>(value: T): Ok<T> {
|
|
23
|
+
return { ok: true, value }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function err<E>(error: E): Err<E> {
|
|
27
|
+
return { ok: false, error }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isOk<T, E>(result: Result<T, E>): result is Ok<T> {
|
|
31
|
+
return result.ok
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isErr<T, E>(result: Result<T, E>): result is Err<E> {
|
|
35
|
+
return !result.ok
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function to<T, E = unknown>(promise: Promise<T>): Promise<Result<Awaited<T>, E>> {
|
|
39
|
+
try {
|
|
40
|
+
return ok(await promise)
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return err(error as E)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function unwrap<S extends Result<any, any>>(
|
|
47
|
+
result: S
|
|
48
|
+
): S extends Ok<infer T> ? T : S extends Err<infer E> ? E : never {
|
|
49
|
+
return result.ok ? result.value : result.error
|
|
50
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vite-plus/test'
|
|
2
|
+
|
|
3
|
+
import { defineBatchEmitter } from '..'
|
|
4
|
+
|
|
5
|
+
describe('BatchEmitter 单元测试', () => {
|
|
6
|
+
it('立即返回单个 id,当 delay <= 0', async () => {
|
|
7
|
+
const batchEmitter = defineBatchEmitter().make()
|
|
8
|
+
const { batchEmit } = batchEmitter
|
|
9
|
+
const result = await batchEmit('a', 0)
|
|
10
|
+
expect(result).toEqual(['a'])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('延迟后批量返回多个 id', async () => {
|
|
14
|
+
const batchEmitter = defineBatchEmitter().make()
|
|
15
|
+
const { batchEmit } = batchEmitter
|
|
16
|
+
const p1 = batchEmit('a', 10)
|
|
17
|
+
const p2 = batchEmit('b', 10)
|
|
18
|
+
const p3 = batchEmit('c', 10)
|
|
19
|
+
|
|
20
|
+
const results = await Promise.all([p1, p2, p3])
|
|
21
|
+
// 三个 promise 都应该 resolve 同一个批次
|
|
22
|
+
results.forEach(r => expect(r).toEqual(['a', 'b', 'c']))
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('flush 可以同步清空队列并调用 onFlushed', async () => {
|
|
26
|
+
const onFlushed = vi.fn()
|
|
27
|
+
const batchEmitter = defineBatchEmitter(onFlushed).make()
|
|
28
|
+
const { batchEmit, flush } = batchEmitter
|
|
29
|
+
|
|
30
|
+
const p1 = batchEmit(1, 100)
|
|
31
|
+
const p2 = batchEmit(2, 100)
|
|
32
|
+
|
|
33
|
+
await flush()
|
|
34
|
+
|
|
35
|
+
const results = await Promise.all([p1, p2])
|
|
36
|
+
expect(results[0]).toEqual([1, 2])
|
|
37
|
+
expect(results[1]).toEqual([1, 2])
|
|
38
|
+
expect(onFlushed).toHaveBeenCalledWith([1, 2])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('flush 在队列为空时不做任何事', async () => {
|
|
42
|
+
const onFlushed = vi.fn()
|
|
43
|
+
const batchEmitter = defineBatchEmitter(onFlushed).make()
|
|
44
|
+
const { flush } = batchEmitter
|
|
45
|
+
await flush()
|
|
46
|
+
expect(onFlushed).not.toHaveBeenCalled()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
|
2
|
+
|
|
3
|
+
import { definePlugin, type PluginMade } from '../core'
|
|
4
|
+
|
|
5
|
+
describe('definePlugin 单元测试', () => {
|
|
6
|
+
const localStorageMock = (() => {
|
|
7
|
+
let store: Record<string, string> = {}
|
|
8
|
+
return {
|
|
9
|
+
getItem: (key: string) => store[key] || null,
|
|
10
|
+
setItem: (key: string, value: string) => {
|
|
11
|
+
store[key] = value.toString()
|
|
12
|
+
},
|
|
13
|
+
clear: () => {
|
|
14
|
+
store = {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
})()
|
|
18
|
+
vi.stubGlobal('localStorage', localStorageMock)
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
localStorage.clear()
|
|
22
|
+
vi.restoreAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('make', () => {
|
|
26
|
+
it('应当实例化插件,并且能够调用插件的属性方法', () => {
|
|
27
|
+
function defineTracker() {
|
|
28
|
+
return definePlugin(() => ({
|
|
29
|
+
track: (event: string) => console.log(`tracking ${event}`)
|
|
30
|
+
}))
|
|
31
|
+
}
|
|
32
|
+
const tracker = defineTracker().make()
|
|
33
|
+
expect(tracker).toHaveProperty('track')
|
|
34
|
+
expect(typeof tracker.track).toBe('function')
|
|
35
|
+
|
|
36
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
37
|
+
tracker.track('test event')
|
|
38
|
+
expect(spy).toHaveBeenCalled()
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('use', () => {
|
|
43
|
+
const defineStore = (initial: Record<string, number>) =>
|
|
44
|
+
definePlugin(() => {
|
|
45
|
+
let store: Record<string, number> = initial
|
|
46
|
+
return {
|
|
47
|
+
setStore(newStore: Record<string, number>) {
|
|
48
|
+
store = newStore
|
|
49
|
+
},
|
|
50
|
+
get(key: string) {
|
|
51
|
+
return store[key]
|
|
52
|
+
},
|
|
53
|
+
set(key: string, val: number) {
|
|
54
|
+
store[key] = val
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const defineLogger = () =>
|
|
60
|
+
definePlugin((ctx: PluginMade<typeof defineStore>) => ({
|
|
61
|
+
set: (key: string, val: number) => {
|
|
62
|
+
ctx.set(key, val)
|
|
63
|
+
console.log(`set ${key} = ${val}.`)
|
|
64
|
+
}
|
|
65
|
+
}))
|
|
66
|
+
|
|
67
|
+
const definePersist = (persitKey = 'persit-store') =>
|
|
68
|
+
definePlugin((ctx: PluginMade<typeof defineStore>) => {
|
|
69
|
+
const cache = localStorage.getItem(persitKey)
|
|
70
|
+
if (cache) ctx.setStore(JSON.parse(cache))
|
|
71
|
+
return {
|
|
72
|
+
setStore(newStore: Record<string, number>) {
|
|
73
|
+
ctx.setStore(newStore)
|
|
74
|
+
localStorage.setItem(persitKey, JSON.stringify(newStore))
|
|
75
|
+
},
|
|
76
|
+
set(key: string, val: number) {
|
|
77
|
+
ctx.set(key, val)
|
|
78
|
+
const cache: Record<string, number> = JSON.parse(localStorage.getItem(persitKey) ?? '{}')
|
|
79
|
+
cache[key] = val
|
|
80
|
+
localStorage.setItem(persitKey, JSON.stringify(cache))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('应当正确组合 Store, Logger 和 Persist 逻辑', () => {
|
|
86
|
+
const logSpy = vi.spyOn(console, 'log')
|
|
87
|
+
|
|
88
|
+
const store = defineStore({ a: 1, b: 2 }).use(defineLogger()).use(definePersist()).make()
|
|
89
|
+
|
|
90
|
+
expect(store.get('a')).toBe(1)
|
|
91
|
+
|
|
92
|
+
// 调用顺序: Persist.set -> Logger.set -> Store.set
|
|
93
|
+
store.set('a', 100)
|
|
94
|
+
|
|
95
|
+
expect(store.get('a')).toBe(100)
|
|
96
|
+
expect(logSpy).toHaveBeenCalledWith('set a = 100.')
|
|
97
|
+
|
|
98
|
+
const cached = JSON.parse(localStorage.getItem('persit-store') || '{}')
|
|
99
|
+
expect(cached.a).toBe(100)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('应当在初始化时从持久化恢复数据', () => {
|
|
103
|
+
localStorage.setItem('persit-store', JSON.stringify({ a: 999 }))
|
|
104
|
+
|
|
105
|
+
const store = defineStore({ a: 1 }).use(definePersist()).make()
|
|
106
|
+
|
|
107
|
+
expect(store.get('a')).toBe(999)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vite-plus/test'
|
|
2
|
+
|
|
3
|
+
import { defineEventEmitter } from '../event-emitter'
|
|
4
|
+
|
|
5
|
+
describe('EventEmitter 单元测试', () => {
|
|
6
|
+
it('应当支持注册和触发事件回调', () => {
|
|
7
|
+
const logSpy = vi.spyOn(console, 'log')
|
|
8
|
+
|
|
9
|
+
const eventEmitter = defineEventEmitter<{
|
|
10
|
+
click: [string]
|
|
11
|
+
}>().make()
|
|
12
|
+
eventEmitter.on('click', e => {
|
|
13
|
+
console.log(`${e} click 1`)
|
|
14
|
+
})
|
|
15
|
+
eventEmitter.on('click', e => {
|
|
16
|
+
console.log(`${e} click 2`)
|
|
17
|
+
})
|
|
18
|
+
eventEmitter.emit('click', 'Tom')
|
|
19
|
+
|
|
20
|
+
expect(logSpy).toHaveBeenCalledWith('Tom click 1')
|
|
21
|
+
expect(logSpy).toHaveBeenCalledWith('Tom click 2')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('应当支持传递 rest 参数', () => {
|
|
25
|
+
const eventPlugin = defineEventEmitter<{
|
|
26
|
+
'user:update': [number, string, 'admin' | 'user']
|
|
27
|
+
'app:start': []
|
|
28
|
+
}>()
|
|
29
|
+
const app = eventPlugin.make()
|
|
30
|
+
const users: string[] = []
|
|
31
|
+
|
|
32
|
+
app.on('user:update', (id, name, role) => {
|
|
33
|
+
const user = `ID: ${id}, Name: ${name}, Role: ${role}`
|
|
34
|
+
users.push(user)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
app.emit('user:update', 1, 'Tom', 'admin')
|
|
38
|
+
|
|
39
|
+
expect(users).toEqual(['ID: 1, Name: Tom, Role: admin'])
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { definePlugin } from './core'
|
|
2
|
+
|
|
3
|
+
type Resolve<S> = (queue: S[]) => void
|
|
4
|
+
|
|
5
|
+
export function defineBatchEmitter<S>(onFlushed?: (queue: S[]) => Promise<void>) {
|
|
6
|
+
return definePlugin(() => {
|
|
7
|
+
let queue: S[] = []
|
|
8
|
+
let resolves: Resolve<S>[] = []
|
|
9
|
+
let timerId: ReturnType<typeof setTimeout> | null = null
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 批量发出,延迟执行
|
|
13
|
+
*/
|
|
14
|
+
async function batchEmit(data: S, batchingDelay = 0): Promise<S[]> {
|
|
15
|
+
if (batchingDelay <= 0) {
|
|
16
|
+
if (onFlushed) await onFlushed([data])
|
|
17
|
+
return Promise.resolve([data])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
resolves.push(resolve)
|
|
22
|
+
|
|
23
|
+
if (queue.push(data) === 1) {
|
|
24
|
+
timerId = setTimeout(() => {
|
|
25
|
+
try {
|
|
26
|
+
void flush()
|
|
27
|
+
} catch (error) {
|
|
28
|
+
reject(error)
|
|
29
|
+
}
|
|
30
|
+
}, batchingDelay)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 立即清空并执行当前的队列
|
|
37
|
+
*/
|
|
38
|
+
async function flush() {
|
|
39
|
+
if (!queue.length) return
|
|
40
|
+
|
|
41
|
+
// 先快照一份,防止后续 push 污染当前批次,消除异步重入(Re-entrancy)
|
|
42
|
+
const currentQueue = [...queue]
|
|
43
|
+
const currentResolves = [...resolves]
|
|
44
|
+
queue = []
|
|
45
|
+
resolves = []
|
|
46
|
+
if (timerId) {
|
|
47
|
+
clearTimeout(timerId)
|
|
48
|
+
timerId = null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (onFlushed) await onFlushed(currentQueue)
|
|
52
|
+
|
|
53
|
+
let fn: Resolve<S> | undefined
|
|
54
|
+
while ((fn = currentResolves.shift())) {
|
|
55
|
+
fn(currentQueue)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { batchEmit, flush }
|
|
60
|
+
})
|
|
61
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface Plugin<C extends object, D extends object> {
|
|
2
|
+
readonly use: <U extends object, E extends object>(plugin: Plugin<U, E>) => Plugin<C & U, D & E>
|
|
3
|
+
readonly make: <T extends D>(ctx?: T) => T & C
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function definePlugin<C extends object, D extends object>(setup: (ctx: D) => C): Plugin<C, D> {
|
|
7
|
+
function use<U extends object, E extends object>(plugin: Plugin<U, E>): Plugin<C & U, D & E> {
|
|
8
|
+
return definePlugin<C & U, D & E>(ctx => {
|
|
9
|
+
return plugin.make({
|
|
10
|
+
...ctx,
|
|
11
|
+
...setup(ctx)
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function make<T extends D>(ctx = {} as T): T & C {
|
|
17
|
+
return {
|
|
18
|
+
...ctx,
|
|
19
|
+
...setup(ctx)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
use,
|
|
25
|
+
make
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 提取插件实例化的类型 (C & D)
|
|
31
|
+
* T 可以是 Plugin 实例,也可以是返回 Plugin 实例的函数
|
|
32
|
+
*/
|
|
33
|
+
export type PluginMade<T> = T extends (...args: any[]) => Plugin<infer C, infer D>
|
|
34
|
+
? C & D
|
|
35
|
+
: T extends Plugin<infer C, infer D>
|
|
36
|
+
? C & D
|
|
37
|
+
: never
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { definePlugin } from './core'
|
|
2
|
+
|
|
3
|
+
export function defineEventEmitter<E extends Record<string, unknown[]>>() {
|
|
4
|
+
return definePlugin(() => {
|
|
5
|
+
const bus = new EventTarget()
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
on<K extends keyof E>(type: K & string, handler: (...args: E[K]) => void, options?: AddEventListenerOptions) {
|
|
9
|
+
const listener = (e: Event) => {
|
|
10
|
+
const args = (e as CustomEvent).detail as E[K]
|
|
11
|
+
handler(...args)
|
|
12
|
+
}
|
|
13
|
+
bus.addEventListener(type, listener, options)
|
|
14
|
+
// off fn
|
|
15
|
+
return () => bus.removeEventListener(type, listener)
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
// 支持传入多个参数
|
|
19
|
+
emit<K extends keyof E>(type: K & string, ...args: E[K]) {
|
|
20
|
+
const event = new CustomEvent(type, { detail: args })
|
|
21
|
+
bus.dispatchEvent(event)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vite-plus/test'
|
|
2
|
+
|
|
3
|
+
import { randomFloat, randomHex, randomRgb } from '..'
|
|
4
|
+
|
|
5
|
+
describe('random 单元测试', () => {
|
|
6
|
+
describe('randomFloat', () => {
|
|
7
|
+
it('结果应在指定的范围内', () => {
|
|
8
|
+
const min = 1.1
|
|
9
|
+
const max = 5.5
|
|
10
|
+
for (let i = 0; i < 100; i++) {
|
|
11
|
+
const result = randomFloat(min, max)
|
|
12
|
+
expect(result).toBeGreaterThanOrEqual(min)
|
|
13
|
+
// 考虑到 Math.random() 是 [0, 1),结果理论上小于 max
|
|
14
|
+
expect(result).toBeLessThanOrEqual(max)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('即便 min 和 max 传反了也应当正常工作', () => {
|
|
19
|
+
const min = 10
|
|
20
|
+
const max = 1
|
|
21
|
+
const result = randomFloat(min, max)
|
|
22
|
+
expect(result).toBeGreaterThanOrEqual(1)
|
|
23
|
+
expect(result).toBeLessThanOrEqual(10)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('当 min 等于 max 时应返回该数值', () => {
|
|
27
|
+
const result = randomFloat(5, 5)
|
|
28
|
+
expect(result).toBe(5)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('randomRgb', () => {
|
|
33
|
+
it('应当返回正确的 rgb(r, g, b) 格式', () => {
|
|
34
|
+
// 允许逗号后有或没有空格
|
|
35
|
+
const rgbRegex = /^rgb\(\d{1,3},\s?\d{1,3},\s?\d{1,3}\)$/
|
|
36
|
+
for (let i = 0; i < 50; i++) {
|
|
37
|
+
const result = randomRgb()
|
|
38
|
+
expect(result).toMatch(rgbRegex)
|
|
39
|
+
|
|
40
|
+
// 进一步校验数值范围
|
|
41
|
+
const colors = result.match(/\d+/g)?.map(Number)
|
|
42
|
+
colors?.forEach(c => {
|
|
43
|
+
expect(c).toBeGreaterThanOrEqual(0)
|
|
44
|
+
expect(c).toBeLessThanOrEqual(255)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('randomHex', () => {
|
|
51
|
+
it('应当返回正确的十六进制格式', () => {
|
|
52
|
+
const hexRegex = /^#[0-9a-f]{6}$/
|
|
53
|
+
for (let i = 0; i < 50; i++) {
|
|
54
|
+
const result = randomHex()
|
|
55
|
+
expect(result).toMatch(hexRegex)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('生成的十六进制应当是小写的', () => {
|
|
60
|
+
const result = randomHex()
|
|
61
|
+
// 检查不含大写字母:如果包含大写字母则测试失败
|
|
62
|
+
expect(result).not.toMatch(/[A-Z]/)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('随机性校验 (Randomness)', () => {
|
|
67
|
+
it('连续调用不应产生相同的结果', () => {
|
|
68
|
+
const results = new Set()
|
|
69
|
+
const count = 100
|
|
70
|
+
for (let i = 0; i < count; i++) {
|
|
71
|
+
results.add(randomHex())
|
|
72
|
+
}
|
|
73
|
+
// 16777216 种颜色选 100 次,碰撞概率微乎其微
|
|
74
|
+
expect(results.size).toBe(count)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 随机整数 [min, max]
|
|
3
|
+
* @param min 最小值
|
|
4
|
+
* @param max 最大值
|
|
5
|
+
*/
|
|
6
|
+
export function random(min: number, max: number): number {
|
|
7
|
+
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 随机浮点数 [min, max)
|
|
12
|
+
* @param min 最小值
|
|
13
|
+
* @param max 最大值
|
|
14
|
+
* @param precision 保留小数位,不传则返回原始浮点数
|
|
15
|
+
*/
|
|
16
|
+
export function randomFloat(min: number, max: number): number {
|
|
17
|
+
const lower = Math.min(min, max)
|
|
18
|
+
const upper = Math.max(min, max)
|
|
19
|
+
return lower + Math.random() * (upper - lower)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 生成随机的 RGB 颜色代码
|
|
24
|
+
* @returns RGB 颜色代码,例如 rgb(255, 0, 0)
|
|
25
|
+
*/
|
|
26
|
+
export function randomRgb(): string {
|
|
27
|
+
return `rgb(${random(0, 255)}, ${random(0, 255)}, ${random(0, 255)})`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 生成随机的十六进制颜色代码
|
|
32
|
+
* @returns 十六进制颜色代码,例如 #ff0000
|
|
33
|
+
*/
|
|
34
|
+
export function randomHex(): string {
|
|
35
|
+
const color = Math.floor(Math.random() * 0x1000000).toString(16)
|
|
36
|
+
return '#' + color.padStart(6, '0')
|
|
37
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
|
2
|
+
|
|
3
|
+
import { createControllableInterval } from '..'
|
|
4
|
+
|
|
5
|
+
describe('timer 单元测试', () => {
|
|
6
|
+
describe('ControllableInterval', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.useFakeTimers()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('应当能正常循环执行', () => {
|
|
12
|
+
const cb = vi.fn()
|
|
13
|
+
const timer = createControllableInterval(cb, 1000)
|
|
14
|
+
timer.start()
|
|
15
|
+
|
|
16
|
+
vi.advanceTimersByTime(3500)
|
|
17
|
+
expect(cb).toHaveBeenCalledTimes(3)
|
|
18
|
+
timer.stop()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('暂停后不应触发回调', () => {
|
|
22
|
+
const cb = vi.fn()
|
|
23
|
+
const timer = createControllableInterval(cb, 1000)
|
|
24
|
+
timer.start()
|
|
25
|
+
|
|
26
|
+
vi.advanceTimersByTime(500)
|
|
27
|
+
timer.pause()
|
|
28
|
+
vi.advanceTimersByTime(1000)
|
|
29
|
+
|
|
30
|
+
expect(cb).not.toHaveBeenCalled()
|
|
31
|
+
timer.stop()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('恢复后应先补全剩余时间', () => {
|
|
35
|
+
const cb = vi.fn()
|
|
36
|
+
const timer = createControllableInterval(cb, 1000)
|
|
37
|
+
timer.start()
|
|
38
|
+
|
|
39
|
+
vi.advanceTimersByTime(800) // 此时还剩 200ms
|
|
40
|
+
timer.pause()
|
|
41
|
+
|
|
42
|
+
timer.resume()
|
|
43
|
+
vi.advanceTimersByTime(150)
|
|
44
|
+
expect(cb).not.toHaveBeenCalled() // 还没到 200ms,不触发
|
|
45
|
+
|
|
46
|
+
vi.advanceTimersByTime(60) // 超过 200ms 了
|
|
47
|
+
expect(cb).toHaveBeenCalledTimes(1)
|
|
48
|
+
timer.stop()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file timer
|
|
3
|
+
* @description 增强型定时器工具(ControllableInterval)
|
|
4
|
+
* 特性:
|
|
5
|
+
* 1.进度保留:暂停 (pause) 时记录剩余时间,恢复 (resume) 时补全当前周期,而非重新计时
|
|
6
|
+
* 2.状态可控:支持随时 start、pause、resume、stop
|
|
7
|
+
* 3.链路稳健:基于递归 setTimeout 实现,避免原生 setInterval 在回调耗时过长时的“堆积”效应
|
|
8
|
+
* 4.环境兼容:适配浏览器与 Node.js (SSR),自动处理 Timeout 类型差异
|
|
9
|
+
* @example
|
|
10
|
+
* const timer = createControllableInterval(() => {
|
|
11
|
+
* console.log('tick')
|
|
12
|
+
* }, 1000))
|
|
13
|
+
* timer.start()
|
|
14
|
+
* setTimeout(() => {
|
|
15
|
+
* timer.pause() // 运行 800ms 后暂停,剩余 200ms
|
|
16
|
+
* setTimeout(() => {
|
|
17
|
+
* timer.resume() // 200ms 后立即触发回调,随后回归 1000ms 周期
|
|
18
|
+
* }, 500)
|
|
19
|
+
* }, 800)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { definePlugin } from '@/plugin'
|
|
23
|
+
|
|
24
|
+
export function defineControllableInterval(callback: () => void, interval: number) {
|
|
25
|
+
return definePlugin(() => {
|
|
26
|
+
let timerId: ReturnType<typeof setTimeout> | null = null
|
|
27
|
+
let isPaused = false
|
|
28
|
+
let remainingTime = 0
|
|
29
|
+
let lastStartTime = 0
|
|
30
|
+
|
|
31
|
+
function tick(delay: number) {
|
|
32
|
+
lastStartTime = Date.now()
|
|
33
|
+
timerId = setTimeout(() => {
|
|
34
|
+
callback()
|
|
35
|
+
// 只有在没被暂停的情况下,才继续下一次循环
|
|
36
|
+
if (!isPaused) tick(interval)
|
|
37
|
+
}, delay)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function start() {
|
|
41
|
+
if (timerId || isPaused) return
|
|
42
|
+
|
|
43
|
+
tick(interval)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pause() {
|
|
47
|
+
if (isPaused || !timerId) return
|
|
48
|
+
|
|
49
|
+
isPaused = true
|
|
50
|
+
clearTimeout(timerId)
|
|
51
|
+
timerId = null
|
|
52
|
+
|
|
53
|
+
// 计算当前这一轮还剩多少时间没跑完
|
|
54
|
+
const diff = Date.now() - lastStartTime
|
|
55
|
+
remainingTime = Math.max(0, interval - diff)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resume() {
|
|
59
|
+
if (!isPaused) return
|
|
60
|
+
|
|
61
|
+
isPaused = false
|
|
62
|
+
// 恢复执行:先跑完上一轮剩下的时间
|
|
63
|
+
tick(remainingTime || interval)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function stop() {
|
|
67
|
+
if (timerId) clearTimeout(timerId)
|
|
68
|
+
|
|
69
|
+
timerId = null
|
|
70
|
+
isPaused = false
|
|
71
|
+
remainingTime = 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
tick,
|
|
76
|
+
start,
|
|
77
|
+
pause,
|
|
78
|
+
resume,
|
|
79
|
+
stop
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createControllableInterval(callback: () => void, interval: number) {
|
|
85
|
+
return defineControllableInterval(callback, interval).make()
|
|
86
|
+
}
|