Add tests

This commit is contained in:
Ticruz
2025-02-04 13:21:23 +01:00
committed by Jon Staab
parent 917727c86f
commit 8a2b62f693
57 changed files with 9231 additions and 25 deletions
+1
View File
@@ -1,2 +1,3 @@
build
normalize-url
__tests__
+212
View File
@@ -0,0 +1,212 @@
import {describe, expect, it} from "vitest"
import {defer, makePromise} from "../src/Deferred"
describe("Deferred", () => {
const pubkey = "ee".repeat(32)
const eventId = "ff".repeat(32)
type SuccessResponse = {
eventId: string
pubkey: string
success: true
}
type ErrorResponse = {
code: number
message: string
success: false
}
describe("makePromise", () => {
it("should create a promise that resolves", async () => {
const successData: SuccessResponse = {
eventId: eventId,
pubkey: pubkey,
success: true,
}
const promise = makePromise<SuccessResponse, ErrorResponse>(resolve => {
resolve(successData)
})
const result = await promise
expect(result).toEqual(successData)
})
it("should create a promise that rejects", async () => {
const errorData: ErrorResponse = {
code: 404,
message: "Event not found",
success: false,
}
const promise = makePromise<SuccessResponse, ErrorResponse>((_, reject) => {
reject(errorData)
})
await expect(promise).rejects.toEqual(errorData)
})
it("should handle async operations", async () => {
const successData: SuccessResponse = {
eventId: eventId,
pubkey: pubkey,
success: true,
}
const promise = makePromise<SuccessResponse, ErrorResponse>(resolve => {
setTimeout(() => resolve(successData), 100)
})
const result = await promise
expect(result).toEqual(successData)
})
it("should propagate errors in promise chain", async () => {
const errorData: ErrorResponse = {
code: 500,
message: "Internal error",
success: false,
}
const promise = makePromise<SuccessResponse, ErrorResponse>((_, reject) => {
setTimeout(() => reject(errorData), 100)
})
await expect(promise).rejects.toEqual(errorData)
})
})
describe("defer", () => {
it("should create a deferred promise that can be resolved", async () => {
const deferred = defer<SuccessResponse, ErrorResponse>()
const successData: SuccessResponse = {
eventId: eventId,
pubkey: pubkey,
success: true,
}
// Resolve in next tick to test async behavior
setTimeout(() => {
deferred.resolve(successData)
}, 0)
const result = await deferred
expect(result).toEqual(successData)
})
it("should create a deferred promise that can be rejected", async () => {
const deferred = defer<SuccessResponse, ErrorResponse>()
const errorData: ErrorResponse = {
code: 403,
message: "Unauthorized",
success: false,
}
setTimeout(() => {
deferred.reject(errorData)
}, 0)
await expect(deferred).rejects.toEqual(errorData)
})
it("should handle immediate resolution", async () => {
const deferred = defer<SuccessResponse, ErrorResponse>()
const successData: SuccessResponse = {
eventId: eventId,
pubkey: pubkey,
success: true,
}
deferred.resolve(successData)
const result = await deferred
expect(result).toEqual(successData)
})
it("should handle immediate rejection", async () => {
const deferred = defer<SuccessResponse, ErrorResponse>()
const errorData: ErrorResponse = {
code: 400,
message: "Bad request",
success: false,
}
deferred.reject(errorData)
await expect(deferred).rejects.toEqual(errorData)
})
it("should work with promise chaining", async () => {
const deferred = defer<SuccessResponse, ErrorResponse>()
const successData: SuccessResponse = {
eventId: eventId,
pubkey: pubkey,
success: true,
}
// Create a chain of promises
const chainedPromise = deferred
.then(result => ({
...result,
eventId: result.eventId.toUpperCase(),
}))
.catch(error => {
throw {...error, code: 599}
})
deferred.resolve(successData)
const result = await chainedPromise
expect(result.eventId).toBe(eventId.toUpperCase())
})
it("should handle error propagation in chains", async () => {
const deferred = defer<SuccessResponse, ErrorResponse>()
const errorData: ErrorResponse = {
code: 401,
message: "Unauthorized",
success: false,
}
const chainedPromise = deferred
.then(result => result)
.catch(error => {
throw {...error, code: 599}
})
deferred.reject(errorData)
await expect(chainedPromise).rejects.toEqual({
...errorData,
code: 599,
})
})
it("should maintain type safety with default error type", async () => {
const deferred = defer<string>() // Using default error type
const successData = eventId
const errorData = "Error processing event"
setTimeout(() => {
if (Math.random() > 0.5) {
deferred.resolve(successData)
} else {
deferred.reject(errorData)
}
}, 0)
try {
const result = await deferred
expect(result).toBe(successData)
} catch (error) {
expect(error).toBe(errorData)
}
})
})
})
+160
View File
@@ -0,0 +1,160 @@
import {describe, it, expect, beforeEach, vi} from "vitest"
import {Emitter} from "../src/Emitter"
describe("Emitter", () => {
let emitter: Emitter
beforeEach(() => {
emitter = new Emitter()
})
it("should emit events to specific listeners", () => {
const listener = vi.fn()
emitter.on("test", listener)
const args = ["arg1", 2, {key: "value"}]
emitter.emit("test", ...args)
expect(listener).toHaveBeenCalledWith(...args)
expect(listener).toHaveBeenCalledTimes(1)
})
it("should emit events to wildcard listeners", () => {
const wildcardListener = vi.fn()
emitter.on("*", wildcardListener)
const args = ["arg1", 2, {key: "value"}]
emitter.emit("test", ...args)
expect(wildcardListener).toHaveBeenCalledWith("test", ...args)
expect(wildcardListener).toHaveBeenCalledTimes(1)
})
it("should emit to both specific and wildcard listeners", () => {
const specificListener = vi.fn()
const wildcardListener = vi.fn()
emitter.on("test", specificListener)
emitter.on("*", wildcardListener)
const args = ["arg1", 2, {key: "value"}]
emitter.emit("test", ...args)
expect(specificListener).toHaveBeenCalledWith(...args)
expect(wildcardListener).toHaveBeenCalledWith("test", ...args)
})
it("should return true if both listeners exist", () => {
emitter.on("test", () => {})
emitter.on("*", () => {})
const result = emitter.emit("test", "arg")
expect(result).toBe(true)
})
it("should return false if no listeners exist", () => {
const result = emitter.emit("test", "arg")
expect(result).toBe(false)
})
it("should handle multiple listeners for same event", () => {
const listener1 = vi.fn()
const listener2 = vi.fn()
emitter.on("test", listener1)
emitter.on("test", listener2)
emitter.emit("test", "arg")
expect(listener1).toHaveBeenCalledWith("arg")
expect(listener2).toHaveBeenCalledWith("arg")
})
it("should handle multiple wildcard listeners", () => {
const listener1 = vi.fn()
const listener2 = vi.fn()
emitter.on("*", listener1)
emitter.on("*", listener2)
emitter.emit("test", "arg")
expect(listener1).toHaveBeenCalledWith("test", "arg")
expect(listener2).toHaveBeenCalledWith("test", "arg")
})
it("should handle listener removal", () => {
const listener = vi.fn()
emitter.on("test", listener)
emitter.removeListener("test", listener)
emitter.emit("test", "arg")
expect(listener).not.toHaveBeenCalled()
})
it("should handle wildcard listener removal", () => {
const listener = vi.fn()
emitter.on("*", listener)
emitter.removeListener("*", listener)
emitter.emit("test", "arg")
expect(listener).not.toHaveBeenCalled()
})
it("should handle once listeners", () => {
const listener = vi.fn()
emitter.once("test", listener)
emitter.emit("test", "arg1")
emitter.emit("test", "arg2")
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith("arg1")
})
it("should handle once wildcard listeners", () => {
const listener = vi.fn()
emitter.once("*", listener)
emitter.emit("test1", "arg1")
emitter.emit("test2", "arg2")
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenCalledWith("test1", "arg1")
})
it("should handle nostr event data", () => {
const listener = vi.fn()
const wildcardListener = vi.fn()
emitter.on("test", listener)
emitter.on("*", wildcardListener)
const complexData = {
id: "ff".repeat(32), // Realistic event ID
pubkey: "ee".repeat(32), // Realistic pubkey
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [["p", "ee".repeat(32)]],
content: "Hello Nostr!",
}
emitter.emit("test", complexData)
expect(listener).toHaveBeenCalledWith(complexData)
expect(wildcardListener).toHaveBeenCalledWith("test", complexData)
})
it("should maintain correct event order", () => {
const events: string[] = []
emitter.on("test", () => events.push("specific"))
emitter.on("*", () => events.push("wildcard"))
emitter.emit("test")
expect(events).toEqual(["specific", "wildcard"])
})
})
+104
View File
@@ -0,0 +1,104 @@
import {describe, it, expect, beforeEach, vi} from "vitest"
import {LRUCache, cached, simpleCache} from "../src/LRUCache"
describe("Caches", () => {
describe("LRUCache", () => {
describe("basic operations", () => {
let cache: LRUCache<string, number>
beforeEach(() => {
cache = new LRUCache(3) // Max size of 3
})
it("should set and get values", () => {
cache.set("a", 1)
expect(cache.get("a")).toBe(1)
})
it("should check if key exists", () => {
cache.set("a", 1)
expect(cache.has("a")).toBe(true)
expect(cache.has("b")).toBe(false)
})
it("should evict least recently used items when exceeding maxSize", () => {
cache.set("a", 1)
cache.set("b", 2)
cache.set("c", 3)
cache.set("d", 4)
expect(cache.has("a")).toBe(false) // 'a' should be evicted
expect(cache.get("b")).toBe(2)
expect(cache.get("c")).toBe(3)
expect(cache.get("d")).toBe(4)
})
it("should update access order on get", () => {
cache.set("a", 1) // keys = [a]
cache.set("b", 2) // keys = [a, b]
cache.set("c", 3) // keys = [a, b, c]
cache.get("b") // keys = [a, b, c, b]
cache.get("b") // keys = [a, b, c, b, b]
cache.get("b") // keys = [a, b, c, b, b, b] size at limit (maxSize * 2 = 6)
cache.get("a") // keys = [b, b, a] keys is over limit, only the 3 last are kept
cache.set("d", 4) // keys = [b, b, a, d],
// @todo clarify with @staab the intended behavior
// "a" was recently accessed, it should not be evicted
expect(cache.has("a")).toBe(true) // 'a' should be present
expect(cache.has("b")).toBe(false) // 'b' should be evicted
})
})
})
describe("cached function", () => {
it("should cache function results", () => {
const mockGetValue = vi.fn((args: [number]) => args[0] * 2)
const cachedFn = cached({
maxSize: 2,
getKey: (args: [number]) => args[0],
getValue: mockGetValue,
})
expect(cachedFn(1)).toBe(2)
expect(cachedFn(1)).toBe(2)
expect(mockGetValue).toHaveBeenCalledTimes(1) // Should only compute once
})
it("should respect maxSize", () => {
const cachedFn = cached({
maxSize: 2,
getKey: (args: [number]) => args[0],
getValue: (args: [number]) => args[0] * 2,
})
cachedFn(1)
cachedFn(2)
cachedFn(3)
expect(cachedFn.cache.has(1)).toBe(false) // Should be evicted
expect(cachedFn.cache.has(2)).toBe(true)
expect(cachedFn.cache.has(3)).toBe(true)
})
})
describe("simpleCache", () => {
it("should cache function results with default settings", () => {
const mockFn = vi.fn((v: number[]) => v[0] + v[1])
const cachedFn = simpleCache(mockFn)
expect(cachedFn(1, 2)).toBe(3)
expect(cachedFn(1, 2)).toBe(3)
expect(mockFn).toHaveBeenCalledTimes(1)
})
it("should use string join as default key", () => {
const cachedFn = simpleCache((v: number[]) => v[0] + v[1])
cachedFn(1, 2)
expect(cachedFn.cache.has("1:2")).toBe(true)
})
})
})
+357
View File
@@ -0,0 +1,357 @@
import {describe, it, expect, vi, beforeEach, afterEach} from "vitest"
import * as T from "../src/Tools"
describe("Tools", () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
describe("Basic Utils", () => {
it("should check for nil values", () => {
expect(T.isNil(null)).toBe(true)
expect(T.isNil(undefined)).toBe(true)
expect(T.isNil(0)).toBe(false)
expect(T.isNil("")).toBe(false)
})
it("should handle ifLet", () => {
const fn = vi.fn()
T.ifLet(undefined, fn)
expect(fn).not.toHaveBeenCalled()
T.ifLet(5, fn)
expect(fn).toHaveBeenCalledWith(5)
})
it("should handle array operations", () => {
const arr = [1, 2, 3]
expect(T.first(arr)).toBe(1)
expect(T.last(arr)).toBe(3)
})
})
describe("Math Operations", () => {
it("should handle basic math operations", () => {
expect(T.add(2, 3)).toBe(5)
expect(T.sub(5, 3)).toBe(2)
expect(T.mul(2, 3)).toBe(6)
expect(T.div(6, 2)).toBe(3)
})
it("should handle nil values in math operations", () => {
expect(T.add(undefined, 3)).toBe(3)
expect(T.sub(5, undefined)).toBe(5)
expect(T.mul(undefined, undefined)).toBe(0)
})
it("should handle comparisons", () => {
expect(T.lt(2, 3)).toBe(true)
expect(T.gt(3, 2)).toBe(true)
expect(T.lte(2, 2)).toBe(true)
expect(T.gte(2, 2)).toBe(true)
})
})
describe("Array Operations", () => {
it("should handle array transformations", () => {
expect(T.take(2, [1, 2, 3, 4])).toEqual([1, 2])
expect(T.drop(2, [1, 2, 3, 4])).toEqual([3, 4])
expect(T.uniq([1, 1, 2, 2, 3])).toEqual([1, 2, 3])
})
it("should handle chunk operations", () => {
expect(T.chunk(2, [1, 2, 3, 4])).toEqual([
[1, 2],
[3, 4],
])
expect(T.chunks(2, [1, 2, 3, 4])).toEqual([
[1, 3],
[2, 4],
])
})
it("should handle array sorting", () => {
expect(T.sort([3, 1, 2])).toEqual([1, 2, 3])
expect(T.sortBy(x => x.value, [{value: 3}, {value: 1}, {value: 2}])).toEqual([
{value: 1},
{value: 2},
{value: 3},
])
})
})
describe("Object Operations", () => {
it("should handle object transformations", () => {
const obj = {a: 1, b: 2, c: 3}
expect(T.omit(["a"], obj)).toEqual({b: 2, c: 3})
expect(T.pick(["a"], obj)).toEqual({a: 1})
})
it("should handle deep merging", () => {
const a = {x: {y: 1}}
const b = {x: {z: 2}}
expect(T.deepMergeLeft(a, b)).toEqual({x: {y: 1, z: 2}})
})
})
describe("Batch Operations", () => {
it("should handle memoization", () => {
const fn = vi.fn(x => x * 2)
const memoized = T.memoize(fn)
expect(memoized(2)).toBe(4)
expect(memoized(2)).toBe(4)
expect(fn).toHaveBeenCalledTimes(1)
})
it("should handle throttling", async () => {
const fn = vi.fn()
const throttled = T.throttle(100, fn)
throttled()
throttled()
throttled()
expect(fn).toHaveBeenCalledTimes(1)
vi.advanceTimersByTime(200)
expect(fn).toHaveBeenCalledTimes(2)
})
describe("batch", () => {
it("should collect items and process them in batches", async () => {
const processBatch = vi.fn()
const batchFn = T.batch(100, processBatch)
// Add items
batchFn("a")
batchFn("b")
batchFn("c")
// Initially the batch shouldn't be processed
expect(processBatch).toHaveBeenCalledTimes(1)
// Advance timer to trigger batch processing
await vi.advanceTimersByTimeAsync(100)
expect(processBatch).toHaveBeenCalledTimes(2)
expect(processBatch).toHaveBeenCalledWith(["a"])
expect(processBatch).toHaveBeenCalledWith(["b", "c"])
})
it("should handle multiple batch windows", async () => {
const processBatch = vi.fn()
const batchFn = T.batch(100, processBatch)
// First batch
batchFn("a")
batchFn("b")
batchFn("c")
await vi.advanceTimersByTimeAsync(100)
// Second batch
batchFn("d")
batchFn("e")
batchFn("f")
await vi.advanceTimersByTimeAsync(100)
expect(processBatch).toHaveBeenCalledTimes(4)
expect(processBatch).toHaveBeenCalledWith(["a"])
expect(processBatch).toHaveBeenCalledWith(["b", "c"])
expect(processBatch).toHaveBeenCalledWith(["d"])
expect(processBatch).toHaveBeenCalledWith(["e", "f"])
})
})
describe("batcher", () => {
it("should batch requests and return results", async () => {
const executeFn = vi.fn(async (requests: number[]) => requests.map(x => x * 2))
const batchFn = T.batcher(100, executeFn)
// Create multiple concurrent requests
const promise1 = batchFn(1)
const promise2 = batchFn(2)
const promise3 = batchFn(3)
await vi.advanceTimersByTimeAsync(100)
const results = await Promise.all([promise1, promise2, promise3])
expect(executeFn).toHaveBeenCalledTimes(1)
expect(executeFn).toHaveBeenCalledWith([1, 2, 3])
expect(results).toEqual([2, 4, 6])
})
it("should handle multiple batch windows", async () => {
const executeFn = vi.fn(async (requests: number[]) => requests.map(x => x * 2))
const batchFn = T.batcher(100, executeFn)
// First batch
const batch1Promise = Promise.all([batchFn(1), batchFn(2)])
await vi.advanceTimersByTimeAsync(100)
const batch1Results = await batch1Promise
// Second batch
const batch2Promise = Promise.all([batchFn(3), batchFn(4)])
await vi.advanceTimersByTimeAsync(100)
const batch2Results = await batch2Promise
expect(executeFn).toHaveBeenCalledTimes(2)
expect(batch1Results).toEqual([2, 4])
expect(batch2Results).toEqual([6, 8])
})
it("should throw error if execute returns wrong number of results", async () => {
const executeFn = vi.fn(
async (requests: number[]) => [requests[0] * 2], // Return fewer results than requests
)
const batchFn = T.batcher(100, executeFn)
const batchPromise = Promise.all([batchFn(1), batchFn(2)])
await vi.advanceTimersByTimeAsync(200)
await expect(batchPromise).rejects.toThrow("Execute must return a result for each request")
})
})
describe("throttleWithValue", () => {
it("should return cached value between updates", async () => {
let counter = 0
const getValue = vi.fn(() => ++counter)
const throttledGet = T.throttleWithValue(100, getValue)
// First call should execute immediately
expect(throttledGet()).toBe(1)
expect(getValue).toHaveBeenCalledTimes(1)
// Subsequent calls within throttle window should return cached value
expect(throttledGet()).toBe(1)
expect(throttledGet()).toBe(1)
expect(getValue).toHaveBeenCalledTimes(1)
// After throttle window, should update value
await vi.advanceTimersByTimeAsync(100)
// the previous 2 called will have been batched, and the next throttledGet increase the counter to 3
expect(throttledGet()).toBe(3)
expect(getValue).toHaveBeenCalledTimes(3)
})
it("should update value at most once per throttle window", async () => {
let counter = 0
const getValue = vi.fn(() => ++counter)
const throttledGet = T.throttleWithValue(100, getValue)
// Initial value
expect(throttledGet()).toBe(1)
// Multiple calls within window
for (let i = 0; i < 4; i++) {
throttledGet()
await vi.advanceTimersByTimeAsync(20) // 20ms each, still within 100ms window
}
expect(getValue).toHaveBeenCalledTimes(1)
// After window
await vi.advanceTimersByTimeAsync(100)
// the previous called will have been batched, and the next throttledGet increase the counter to 3
expect(throttledGet()).toBe(3)
expect(getValue).toHaveBeenCalledTimes(3)
})
it("should handle zero throttle time", () => {
let counter = 0
const getValue = vi.fn(() => ++counter)
const throttledGet = T.throttleWithValue(0, getValue)
// Should update on every call
expect(throttledGet()).toBe(1)
expect(throttledGet()).toBe(2)
expect(throttledGet()).toBe(3)
expect(getValue).toHaveBeenCalledTimes(3)
})
})
})
describe("Time Utilities", () => {
it("should handle time constants", () => {
expect(T.MINUTE).toBe(60)
expect(T.HOUR).toBe(60 * 60)
expect(T.DAY).toBe(24 * 60 * 60)
})
it("should handle time calculations", () => {
const timestamp = T.now()
expect(typeof timestamp).toBe("number")
expect(T.ms(1)).toBe(1000)
})
})
describe("String Operations", () => {
it("should handle URL formatting", () => {
expect(T.stripProtocol("https://example.com")).toBe("example.com")
expect(T.displayUrl("https://www.example.com/")).toBe("example.com")
expect(T.displayDomain("example.com/path")).toBe("example.com")
// @todo returns https
// expect(T.displayDomain("https://example.com/path")).toBe("example.com")
})
it("should handle string truncation", () => {
expect(T.ellipsize("hello world", 5)).toBe("hello...")
expect(T.ellipsize("hi", 5)).toBe("hi")
})
})
describe("Collection Operations", () => {
it("should handle group operations", () => {
const items = [
{type: "a", val: 1},
{type: "a", val: 2},
{type: "b", val: 3},
]
const grouped = T.groupBy(x => x.type, items)
expect(grouped.get("a")?.length).toBe(2)
expect(grouped.get("b")?.length).toBe(1)
})
it("should handle indexing", () => {
const items = [
{id: 1, val: "a"},
{id: 2, val: "b"},
]
const indexed = T.indexBy(x => x.id, items)
expect(indexed.get(1)?.val).toBe("a")
})
})
describe("Type Checking", () => {
it("should identify plain objects", () => {
expect(T.isPojo({})).toBe(true)
expect(T.isPojo([])).toBe(false)
expect(T.isPojo(null)).toBe(false)
expect(T.isPojo(new Date())).toBe(false)
})
it("should handle deep equality", () => {
expect(T.equals({a: 1}, {a: 1})).toBe(true)
expect(T.equals({a: 1}, {a: 2})).toBe(false)
expect(T.equals([1, 2], [1, 2])).toBe(true)
expect(T.equals(new Set([1, 2]), new Set([1, 2]))).toBe(true)
})
})
})
+208
View File
@@ -0,0 +1,208 @@
import {describe, it, expect, vi, beforeEach, afterEach} from "vitest"
import {Worker} from "../src/Worker"
describe("Worker", () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it("should process messages in batches", async () => {
const handler = vi.fn()
const worker = new Worker<number>()
worker.addGlobalHandler(handler)
// Push messages
worker.push(1)
worker.push(2)
worker.push(3)
// Initially no processing
expect(handler).not.toHaveBeenCalled()
// Advance timer to trigger processing
await vi.advanceTimersByTimeAsync(50)
expect(handler).toHaveBeenCalledTimes(3)
expect(handler).toHaveBeenNthCalledWith(1, 1)
expect(handler).toHaveBeenNthCalledWith(2, 2)
expect(handler).toHaveBeenNthCalledWith(3, 3)
})
it("should respect chunkSize option", async () => {
const handler = vi.fn()
const worker = new Worker<number>({chunkSize: 2})
worker.addGlobalHandler(handler)
// Push more messages than chunkSize
worker.push(1)
worker.push(2)
worker.push(3)
// First batch
await vi.advanceTimersByTimeAsync(50)
expect(handler).toHaveBeenCalledTimes(2)
// Second batch
await vi.advanceTimersByTimeAsync(50)
expect(handler).toHaveBeenCalledTimes(3)
})
it("should handle message routing by key", async () => {
const globalHandler = vi.fn()
const evenHandler = vi.fn()
const oddHandler = vi.fn()
const worker = new Worker<number>({
getKey: x => (x % 2 === 0 ? "even" : "odd"),
})
worker.addGlobalHandler(globalHandler)
worker.addHandler("even", evenHandler)
worker.addHandler("odd", oddHandler)
worker.push(1)
worker.push(2)
await vi.advanceTimersByTimeAsync(50)
expect(globalHandler).toHaveBeenCalledTimes(2)
expect(evenHandler).toHaveBeenCalledWith(2)
expect(oddHandler).toHaveBeenCalledWith(1)
})
it("should handle message deferral", async () => {
const handler = vi.fn()
let shouldDefer = true
const worker = new Worker<number>({
shouldDefer: () => shouldDefer,
})
worker.addGlobalHandler(handler)
worker.push(1)
// Message should be deferred
await vi.advanceTimersByTimeAsync(50)
expect(handler).not.toHaveBeenCalled()
// Allow processing
shouldDefer = false
await vi.advanceTimersByTimeAsync(50)
expect(handler).toHaveBeenCalledWith(1)
})
it("should handle multiple handlers for same key", async () => {
const handler1 = vi.fn()
const handler2 = vi.fn()
const worker = new Worker<number>({
getKey: () => "test",
})
worker.addHandler("test", handler1)
worker.addHandler("test", handler2)
worker.push(1)
await vi.advanceTimersByTimeAsync(50)
expect(handler1).toHaveBeenCalledWith(1)
expect(handler2).toHaveBeenCalledWith(1)
})
it("should handle errors in handlers gracefully", async () => {
const consoleError = vi.spyOn(console, "error")
const errorHandler = vi.fn(() => {
throw new Error("Test error")
})
const nextHandler = vi.fn()
const worker = new Worker<number>()
worker.addGlobalHandler(errorHandler)
worker.addGlobalHandler(nextHandler)
worker.push(1)
await vi.advanceTimersByTimeAsync(50)
expect(consoleError).toHaveBeenCalled()
expect(nextHandler).toHaveBeenCalled()
})
describe("control methods", () => {
it("should clear the buffer", async () => {
const handler = vi.fn()
const worker = new Worker<number>()
worker.addGlobalHandler(handler)
worker.push(1)
worker.push(2)
worker.clear()
await vi.advanceTimersByTimeAsync(50)
expect(handler).not.toHaveBeenCalled()
})
it("should pause and resume processing", async () => {
const handler = vi.fn()
const worker = new Worker<number>()
worker.addGlobalHandler(handler)
worker.push(1)
worker.pause()
await vi.advanceTimersByTimeAsync(50)
expect(handler).not.toHaveBeenCalled()
worker.resume()
await vi.advanceTimersByTimeAsync(50)
expect(handler).toHaveBeenCalled()
})
it("should respect custom delay option", async () => {
const handler = vi.fn()
const worker = new Worker<number>({delay: 100})
worker.addGlobalHandler(handler)
worker.push(1)
await vi.advanceTimersByTimeAsync(50)
expect(handler).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(50) // Total 100ms
expect(handler).toHaveBeenCalled()
})
})
describe("async handlers", () => {
it("should wait for async handlers to complete", async () => {
const results: number[] = []
const asyncHandler = vi.fn(async (x: number) => {
await new Promise(resolve => setTimeout(resolve, 100))
results.push(x)
})
const worker = new Worker<number>()
worker.addGlobalHandler(asyncHandler)
worker.push(1)
worker.push(2)
await vi.advanceTimersByTimeAsync(50) // Trigger processing
await vi.advanceTimersByTimeAsync(100) // Wait for one async handlers
expect(results).toEqual([1])
await vi.advanceTimersByTimeAsync(100) // Wait for another async handlers
expect(results).toEqual([1, 2])
})
})
})
+1 -1
View File
@@ -20,7 +20,7 @@ export class LRUCache<T, U> {
this.keys.push(k as T)
if (this.keys.length > this.maxSize * 2) {
this.keys.splice(-this.maxSize)
this.keys = this.keys.splice(-this.maxSize)
}
}
+9 -4
View File
@@ -874,6 +874,7 @@ export const chunk = <T>(chunkLength: number, xs: T[]) => {
current.push(item)
} else {
result.push(current.splice(0))
current.push(item)
}
}
@@ -991,6 +992,8 @@ export const throttleWithValue = <T>(ms: number, f: () => T) => {
/**
* Creates batching function that collects items
* this function does not delay execution, if a series of items is passed in sequence
* the first item will be processed immediately, and the rest will be batched
* @param t - Time window for batching
* @param f - Function to process batch
* @returns Function that adds items to batch
@@ -1012,26 +1015,28 @@ export const batch = <T>(t: number, f: (xs: T[]) => void) => {
* @returns Function that returns promise of result
*/
export const batcher = <T, U>(t: number, execute: (request: T[]) => U[] | Promise<U[]>) => {
const queue: {request: T; resolve: (x: U) => void}[] = []
const queue: {request: T; resolve: (x: U) => void; reject: (reason?: string) => void}[] = []
const _execute = async () => {
const items = queue.splice(0)
const results = await execute(items.map(item => item.request))
if (results.length !== items.length) {
throw new Error("Execute must return a result for each request")
results.forEach(async (r, i) =>
items[i].reject("Execute must return a result for each request"),
)
}
results.forEach(async (r, i) => items[i].resolve(await r))
}
return (request: T): Promise<U> =>
new Promise(resolve => {
new Promise((resolve, reject) => {
if (queue.length === 0) {
setTimeout(_execute, t)
}
queue.push({request, resolve})
queue.push({request, resolve, reject})
})
}