mirror of
https://github.com/langgenius/dify.git
synced 2026-06-13 04:01:12 +08:00
Co-authored-by: GareArc <garethcxy@dify.ai> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: L1nSn0w <l1nsn0w@qq.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: gigglewang <gigglewang@dify.ai> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
97 lines
2.9 KiB
TypeScript
97 lines
2.9 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
import { dirname, join } from 'node:path'
|
|
import { DIR_PERM, FILE_PERM } from '../config/dir.js'
|
|
|
|
const CACHE_FILE = 'nudge.json'
|
|
const DISK_SCHEMA = 1
|
|
export const WARN_INTERVAL_MS = 24 * 60 * 60 * 1000
|
|
|
|
export type NudgeStore = {
|
|
readonly canWarn: (host: string, now?: Date) => boolean
|
|
readonly markWarned: (host: string, now?: Date) => Promise<void>
|
|
}
|
|
|
|
export type NudgeStoreOptions = {
|
|
readonly configDir: string
|
|
readonly now?: () => Date
|
|
readonly intervalMs?: number
|
|
}
|
|
|
|
type DiskShape = {
|
|
schema?: number
|
|
warned?: Record<string, string>
|
|
}
|
|
|
|
export function nudgeStorePath(configDir: string): string {
|
|
return join(configDir, 'cache', CACHE_FILE)
|
|
}
|
|
|
|
export async function loadNudgeStore(opts: NudgeStoreOptions): Promise<NudgeStore> {
|
|
const path = nudgeStorePath(opts.configDir)
|
|
const intervalMs = opts.intervalMs ?? WARN_INTERVAL_MS
|
|
const clock = opts.now ?? (() => new Date())
|
|
const memory = await readDisk(path)
|
|
|
|
return {
|
|
canWarn: (host, now) => {
|
|
const last = memory.get(host)
|
|
if (last === undefined)
|
|
return true
|
|
const elapsed = Math.max(0, (now ?? clock()).getTime() - last)
|
|
return elapsed >= intervalMs
|
|
},
|
|
markWarned: async (host, now) => {
|
|
const stamp = (now ?? clock()).getTime()
|
|
memory.set(host, stamp)
|
|
// Re-read disk inside the write cycle so concurrent processes touching
|
|
// different hosts don't clobber each other's stamps. Same-host writers
|
|
// converge on a near-identical timestamp, so order doesn't matter.
|
|
const onDisk = await readDisk(path)
|
|
onDisk.set(host, stamp)
|
|
await persist(path, onDisk)
|
|
},
|
|
}
|
|
}
|
|
|
|
async function readDisk(path: string): Promise<Map<string, number>> {
|
|
const out = new Map<string, number>()
|
|
let raw: string
|
|
try {
|
|
raw = await readFile(path, 'utf8')
|
|
}
|
|
catch (err) {
|
|
if ((err as NodeJS.ErrnoException).code === 'ENOENT')
|
|
return out
|
|
throw err
|
|
}
|
|
let parsed: DiskShape
|
|
try {
|
|
parsed = JSON.parse(raw) as DiskShape
|
|
}
|
|
catch {
|
|
return out
|
|
}
|
|
if (parsed.schema !== DISK_SCHEMA || parsed.warned === undefined)
|
|
return out
|
|
for (const [host, iso] of Object.entries(parsed.warned)) {
|
|
const t = Date.parse(iso)
|
|
if (!Number.isNaN(t))
|
|
out.set(host, t)
|
|
}
|
|
return out
|
|
}
|
|
|
|
async function persist(path: string, state: Map<string, number>): Promise<void> {
|
|
const dir = dirname(path)
|
|
await mkdir(dir, { recursive: true, mode: DIR_PERM })
|
|
const disk: DiskShape = { schema: DISK_SCHEMA, warned: {} }
|
|
for (const [host, t] of state)
|
|
disk.warned![host] = new Date(t).toISOString()
|
|
// randomUUID is collision-proof even when two writers stamp the same
|
|
// millisecond — pid+timestamp alone can still collide under tight loops.
|
|
const tmp = `${path}.${randomUUID()}.tmp`
|
|
await writeFile(tmp, JSON.stringify(disk), { mode: FILE_PERM })
|
|
await rename(tmp, path)
|
|
}
|