97 lines
3.1 KiB
JavaScript
97 lines
3.1 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import process from 'process'
|
|
|
|
const root = process.cwd()
|
|
|
|
function fail(title, details = []) {
|
|
console.error(`\n[task-target-states-no-polling-guard] ${title}`)
|
|
for (const line of details) {
|
|
console.error(` - ${line}`)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
|
|
function readFile(relativePath) {
|
|
const fullPath = path.join(root, relativePath)
|
|
if (!fs.existsSync(fullPath)) {
|
|
fail('Missing required file', [relativePath])
|
|
}
|
|
return fs.readFileSync(fullPath, 'utf8')
|
|
}
|
|
|
|
function walk(dir, out = []) {
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
for (const entry of entries) {
|
|
if (entry.name === '.git' || entry.name === '.next' || entry.name === 'node_modules') continue
|
|
const full = path.join(dir, entry.name)
|
|
if (entry.isDirectory()) {
|
|
walk(full, out)
|
|
} else {
|
|
out.push(full)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
function toRel(fullPath) {
|
|
return path.relative(root, fullPath).split(path.sep).join('/')
|
|
}
|
|
|
|
function collectPattern(pattern) {
|
|
const files = walk(path.join(root, 'src'))
|
|
const hits = []
|
|
for (const fullPath of files) {
|
|
if (!fullPath.endsWith('.ts') && !fullPath.endsWith('.tsx')) continue
|
|
const text = fs.readFileSync(fullPath, 'utf8')
|
|
const lines = text.split('\n')
|
|
for (let i = 0; i < lines.length; i += 1) {
|
|
if (pattern.test(lines[i])) {
|
|
hits.push(`${toRel(fullPath)}:${i + 1}`)
|
|
}
|
|
}
|
|
}
|
|
return hits
|
|
}
|
|
|
|
const refetchIntervalMsHits = collectPattern(/\brefetchIntervalMs\b/)
|
|
if (refetchIntervalMsHits.length > 0) {
|
|
fail('Found forbidden refetchIntervalMs usage', refetchIntervalMsHits)
|
|
}
|
|
|
|
const voiceStagePath =
|
|
'src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/VoiceStage.tsx'
|
|
const voiceStageText = readFile(voiceStagePath)
|
|
if (voiceStageText.includes('setInterval(')) {
|
|
fail('VoiceStage must not use timer polling', [voiceStagePath])
|
|
}
|
|
|
|
const targetStateMapPath = 'src/lib/query/hooks/useTaskTargetStateMap.ts'
|
|
const targetStateMapText = readFile(targetStateMapPath)
|
|
if (!/refetchInterval:\s*false/.test(targetStateMapText)) {
|
|
fail('useTaskTargetStateMap must keep refetchInterval disabled', [targetStateMapPath])
|
|
}
|
|
|
|
const ssePath = 'src/lib/query/hooks/useSSE.ts'
|
|
const sseText = readFile(ssePath)
|
|
const targetStatesInvalidateExprMatch = sseText.match(
|
|
/const shouldInvalidateTargetStates\s*=\s*([\s\S]*?)\n\s*\n/,
|
|
)
|
|
if (!targetStatesInvalidateExprMatch) {
|
|
fail('Unable to locate shouldInvalidateTargetStates expression', [ssePath])
|
|
}
|
|
const targetStatesInvalidateExpr = targetStatesInvalidateExprMatch[1]
|
|
if (!/TASK_EVENT_TYPE\.COMPLETED/.test(targetStatesInvalidateExpr) || !/TASK_EVENT_TYPE\.FAILED/.test(targetStatesInvalidateExpr)) {
|
|
fail('useSSE must invalidate target states only for terminal events', [ssePath])
|
|
}
|
|
if (/TASK_EVENT_TYPE\.CREATED/.test(targetStatesInvalidateExpr)) {
|
|
fail('useSSE target-state invalidation must not include CREATED', [ssePath])
|
|
}
|
|
if (/TASK_EVENT_TYPE\.PROCESSING/.test(targetStatesInvalidateExpr)) {
|
|
fail('useSSE target-state invalidation must not include PROCESSING', [ssePath])
|
|
}
|
|
|
|
console.log('[task-target-states-no-polling-guard] OK')
|