mirror of
https://github.com/Smile-QWQ/SubTracker.git
synced 2026-06-09 15:33:43 +08:00
Port cross-runtime improvements from the cf-worker branch back to main without bringing Worker-free specific tradeoffs. Frontend: - add single-flight request coalescing for save, settings, tag, and AI actions - add explicit saving/loading states for subscription save and AI recognition - unify settings data access behind useSettingsQuery with a shared query key - stop AI autofill from overwriting advance/overdue reminder rules - deep-clone settings query data before binding form state so nested notification and AI inputs remain editable - refine AI loading copy layout and adjust calendar card wording for clearer UX Backend: - split getAppSettings hot-path reads into narrow getters for login options, AI config, reminder defaults, and notification channel/scan settings - cache auth session secret, stored credentials, and mustChangePassword in memory to avoid repeated password verification work on every request - slim statistics and calendar reads to only fetch fields required by each view - reduce exchange-rate read duplication by reusing resolved base currency instead of re-reading settings unnecessarily - batch Wallos import writes via createMany for tags, subscriptions, and subscription-tag joins, then append subscription order once Tests: - add auth service cache coverage - add Wallos commit batching coverage - add single-flight, settings-form clone, and AI recognition status unit tests - update auth, AI, and statistics tests for the new main-branch implementation strategy Validation: - targeted API tests passed - targeted web tests passed - npm run lint passed - npm run build passed
43 lines
1.3 KiB
TypeScript
43 lines
1.3 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { createSingleFlight } from '@/utils/single-flight'
|
|
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void
|
|
let reject!: (reason?: unknown) => void
|
|
const promise = new Promise<T>((res, rej) => {
|
|
resolve = res
|
|
reject = rej
|
|
})
|
|
return { promise, resolve, reject }
|
|
}
|
|
|
|
describe('createSingleFlight', () => {
|
|
it('coalesces repeated concurrent calls into a single request', async () => {
|
|
const task = deferred<string>()
|
|
const worker = vi.fn(async (value: string) => task.promise)
|
|
const singleFlight = createSingleFlight(worker)
|
|
|
|
const first = singleFlight.run('save')
|
|
const second = singleFlight.run('save')
|
|
|
|
expect(singleFlight.pending).toBe(true)
|
|
expect(worker).toHaveBeenCalledTimes(1)
|
|
expect(first).toBe(second)
|
|
|
|
task.resolve('done')
|
|
|
|
await expect(first).resolves.toBe('done')
|
|
expect(singleFlight.pending).toBe(false)
|
|
})
|
|
|
|
it('allows a new call after the previous one settles', async () => {
|
|
const worker = vi.fn(async (value: string) => value)
|
|
const singleFlight = createSingleFlight(worker)
|
|
|
|
await expect(singleFlight.run('first')).resolves.toBe('first')
|
|
await expect(singleFlight.run('second')).resolves.toBe('second')
|
|
|
|
expect(worker).toHaveBeenCalledTimes(2)
|
|
})
|
|
})
|