9bf25765dd
新增纯 TS 轮询基线组件 task-polling-baseline:累计轮询请求数、每次 批量响应的总字节与单次最大响应、按条目数/字节双上限的有界内存缓存 (超限驱逐最旧条目),失败路径零状态变更。useTaskProgressLoop 新增 可选 onBaseline 选项接入基线(默认零行为变化)。测试用 Node 24 原生 type-stripping + node:test 运行(npm test),8 个用例覆盖默认路径、 批量、幂等、空输入、单元素、限流溢出、非法输入与失败恢复。
162 lines
6.2 KiB
TypeScript
162 lines
6.2 KiB
TypeScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { createTaskPollingBaseline } from '../src/shared/task-polling-baseline.ts'
|
|
|
|
interface FakeDetail {
|
|
id: number
|
|
name: string
|
|
status: string
|
|
}
|
|
|
|
function detail(id: number): FakeDetail {
|
|
return { id, name: `task-${id}`, status: id % 2 === 0 ? 'SUCCESS' : 'RUNNING' }
|
|
}
|
|
|
|
const byId = (d: FakeDetail) => d.id
|
|
const enc = (v: unknown) => new TextEncoder().encode(JSON.stringify(v)).byteLength
|
|
|
|
test('test_task_081_polling_frontend_normal_default_path', () => {
|
|
const baseline = createTaskPollingBaseline()
|
|
baseline.recordRequest()
|
|
baseline.recordRequest()
|
|
baseline.recordRequest()
|
|
const inserted = baseline.recordResponseItems([detail(1), detail(2)], byId)
|
|
assert.equal(inserted, 2)
|
|
const stats = baseline.stats()
|
|
assert.equal(stats.requestCount, 3)
|
|
assert.equal(stats.entryCount, 2)
|
|
assert.equal(stats.droppedCount, 0)
|
|
assert.ok(stats.cachedBytes > 0)
|
|
assert.ok(stats.cachedBytes <= 1024 * 1024)
|
|
assert.equal(stats.totalResponseBytes, enc([detail(1), detail(2)]))
|
|
assert.equal(stats.largestResponseBytes, stats.totalResponseBytes)
|
|
assert.deepEqual(stats.entryKeys, [1, 2])
|
|
})
|
|
|
|
test('test_task_081_polling_frontend_normal_multiple_items', () => {
|
|
const baseline = createTaskPollingBaseline()
|
|
const items = Array.from({ length: 10 }, (_, i) => detail(i + 1))
|
|
const inserted = baseline.recordResponseItems(items, byId)
|
|
assert.equal(inserted, 10)
|
|
const stats = baseline.stats()
|
|
assert.equal(stats.entryCount, 10)
|
|
assert.deepEqual(stats.entryKeys, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
|
// 覆盖更新不改变首次到达顺序
|
|
baseline.recordResponseItems([detail(5)], byId)
|
|
const after = baseline.stats()
|
|
assert.equal(after.entryCount, 10)
|
|
assert.deepEqual(after.entryKeys, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
|
assert.equal(after.totalResponseBytes, enc(items) + enc([detail(5)]))
|
|
assert.equal(after.largestResponseBytes, enc(items))
|
|
})
|
|
|
|
test('test_task_081_polling_frontend_normal_repeated_operation_is_idempotent', () => {
|
|
const baseline = createTaskPollingBaseline()
|
|
const items = [detail(1), detail(2), detail(3)]
|
|
baseline.recordResponseItems(items, byId)
|
|
baseline.recordResponseItems(items, byId)
|
|
const stats = baseline.stats()
|
|
assert.equal(stats.entryCount, 3)
|
|
assert.equal(stats.droppedCount, 0)
|
|
const single = createTaskPollingBaseline()
|
|
single.recordResponseItems(items, byId)
|
|
assert.equal(stats.cachedBytes, single.stats().cachedBytes)
|
|
assert.equal(stats.totalResponseBytes, single.stats().totalResponseBytes * 2)
|
|
})
|
|
|
|
test('test_task_081_polling_frontend_boundary_empty_input', () => {
|
|
const baseline = createTaskPollingBaseline()
|
|
assert.equal(baseline.recordResponseItems([], byId), 0)
|
|
assert.equal(baseline.recordResponseItems([], byId), 0)
|
|
const stats = baseline.stats()
|
|
assert.equal(stats.requestCount, 0)
|
|
assert.equal(stats.totalResponseBytes, 0)
|
|
assert.equal(stats.largestResponseBytes, 0)
|
|
assert.equal(stats.entryCount, 0)
|
|
assert.equal(stats.cachedBytes, 0)
|
|
assert.equal(stats.droppedCount, 0)
|
|
assert.deepEqual(stats.entryKeys, [])
|
|
})
|
|
|
|
test('test_task_081_polling_frontend_boundary_single_item', () => {
|
|
const baseline = createTaskPollingBaseline()
|
|
const inserted = baseline.recordResponseItems([detail(7)], byId)
|
|
assert.equal(inserted, 1)
|
|
const stats = baseline.stats()
|
|
assert.equal(stats.entryCount, 1)
|
|
assert.deepEqual(stats.entryKeys, [7])
|
|
assert.equal(stats.cachedBytes, enc(detail(7)) + 64)
|
|
assert.equal(stats.totalResponseBytes, enc([detail(7)]))
|
|
})
|
|
|
|
test('test_task_081_polling_frontend_boundary_limit_and_overflow', () => {
|
|
// 条目数超限:驱逐最旧,缓存有界
|
|
const byEntries = createTaskPollingBaseline({ maxEntries: 3, maxCacheBytes: 1_000_000 })
|
|
byEntries.recordResponseItems([detail(1), detail(2), detail(3), detail(4), detail(5)], byId)
|
|
const s1 = byEntries.stats()
|
|
assert.equal(s1.entryCount, 3)
|
|
assert.deepEqual(s1.entryKeys, [3, 4, 5])
|
|
assert.equal(s1.droppedCount, 2)
|
|
// 单条超过字节上限:直接拒绝
|
|
const small = createTaskPollingBaseline({ maxCacheBytes: 200 })
|
|
const big = { id: 9, name: 'x'.repeat(1000), status: 'RUNNING' }
|
|
const inserted = small.recordResponseItems([big], byId)
|
|
assert.equal(inserted, 0)
|
|
const s2 = small.stats()
|
|
assert.equal(s2.entryCount, 0)
|
|
assert.equal(s2.droppedCount, 1)
|
|
assert.ok(s2.cachedBytes <= 200)
|
|
// 批量累积超限:逐条驱逐最旧直到有界
|
|
const tiny = createTaskPollingBaseline({ maxCacheBytes: 300 })
|
|
tiny.recordResponseItems([detail(1), detail(2), detail(3)], byId)
|
|
const s3 = tiny.stats()
|
|
assert.equal(s3.entryCount, 2)
|
|
assert.deepEqual(s3.entryKeys, [2, 3])
|
|
assert.equal(s3.droppedCount, 1)
|
|
assert.ok(s3.cachedBytes <= 300)
|
|
})
|
|
|
|
test('test_task_081_polling_frontend_invalid_input_rejected', () => {
|
|
const baseline = createTaskPollingBaseline()
|
|
assert.throws(() => baseline.recordResponseItems(null as never, byId), /items 必须是数组/)
|
|
assert.throws(() => baseline.recordResponseItems([detail(1)], null as never), /extractKey 必须是函数/)
|
|
assert.throws(() => createTaskPollingBaseline({ maxEntries: 0 }), /上限配置必须为正数/)
|
|
assert.throws(() => createTaskPollingBaseline({ maxCacheBytes: -1 }), /上限配置必须为正数/)
|
|
// 非法 key 跳过而不是抛错(与轮询层行为一致)
|
|
const inserted = baseline.recordResponseItems(
|
|
[{ id: 0, name: 'x', status: 'y' }, detail(2)],
|
|
byId,
|
|
)
|
|
assert.equal(inserted, 1)
|
|
})
|
|
|
|
test('test_task_081_polling_frontend_dependency_failure_releases_resources', () => {
|
|
const baseline = createTaskPollingBaseline()
|
|
baseline.recordRequest()
|
|
assert.throws(
|
|
() =>
|
|
baseline.recordResponseItems([detail(1)], () => {
|
|
throw new Error('boom')
|
|
}),
|
|
/boom/,
|
|
)
|
|
const after = baseline.stats()
|
|
assert.equal(after.entryCount, 0)
|
|
assert.equal(after.totalResponseBytes, 0)
|
|
assert.equal(after.requestCount, 1)
|
|
assert.equal(after.droppedCount, 0)
|
|
baseline.reset()
|
|
assert.deepEqual(baseline.stats(), {
|
|
requestCount: 0,
|
|
totalResponseBytes: 0,
|
|
largestResponseBytes: 0,
|
|
entryCount: 0,
|
|
cachedBytes: 0,
|
|
droppedCount: 0,
|
|
entryKeys: [],
|
|
})
|
|
// 错误可恢复:修复后同一实例继续工作
|
|
baseline.recordResponseItems([detail(1)], byId)
|
|
assert.equal(baseline.stats().entryCount, 1)
|
|
})
|