Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions lib/db/notion/RateLimiter.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import fs from 'fs'
import path from 'path'

interface QueueItem<T> {
requestFunc: () => Promise<T>
resolve: (value: T) => void
reject: (err: any) => void
reject: (err: unknown) => void
}

interface NodeError extends Error {
code?: string
}

export class RateLimiter {
private queue: QueueItem<any>[] = []
private queue: QueueItem<unknown>[] = []
private inflight = new Set<string>()
private isProcessing = false
private lastRequestTime = 0
Expand Down Expand Up @@ -39,8 +42,9 @@ export class RateLimiter {
try {
fs.writeFileSync(this.lockFilePath, process.pid.toString(), { flag: 'wx' })
return
} catch (err: any) {
if (err.code === 'EEXIST') await new Promise(res => setTimeout(res, 100))
} catch (err) {
const e = err as NodeError
if (e.code === 'EEXIST') await new Promise(res => setTimeout(res, 100))
else throw err
}
}
Expand All @@ -54,19 +58,27 @@ export class RateLimiter {

public enqueue<T>(key: string, requestFunc: () => Promise<T>): Promise<T> {
if (this.inflight.has(key)) {
return new Promise((resolve, reject) => {
return new Promise<T>((resolve, reject) => {
const interval = setInterval(() => {
if (!this.inflight.has(key)) {
clearInterval(interval)
this.enqueue(key, requestFunc).then(resolve).catch(reject)
// 用 void 显式标记忽略:递归 enqueue 自身已串接 resolve/reject
void this.enqueue(key, requestFunc).then(resolve).catch(reject)
}
}, 50)
})
}

return new Promise((resolve, reject) => {
this.queue.push({ requestFunc, resolve, reject })
if (!this.isProcessing) this.processQueue()
return new Promise<T>((resolve, reject) => {
this.queue.push({
requestFunc: requestFunc as () => Promise<unknown>,
resolve: resolve as (value: unknown) => void,
reject
})
if (!this.isProcessing) {
// processQueue 是 async 但这里不 await,需要兜底捕获
void this.processQueue()
}
})
}

Expand Down Expand Up @@ -96,7 +108,7 @@ export class RateLimiter {
this.inflight.add(key)

try {
const result = await requestFunc()
const result: unknown = await requestFunc()
this.lastRequestTime = Date.now()
this.requestCount++
resolve(result)
Expand All @@ -107,7 +119,10 @@ export class RateLimiter {
console.error('限流队列异常', err)
} finally {
this.releaseLock()
setTimeout(() => this.processQueue(), 0)
// 显式忽略下一轮的返回 Promise
setTimeout(() => {
void this.processQueue()
}, 0)
}
}
}
10 changes: 7 additions & 3 deletions lib/db/notion/getNotionAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,17 @@ async function callNotion(methodName, ...args) {

if (globalStore.inflight.has(key)) return globalStore.inflight.get(key)

const execute = async () => original.apply(notion, args)
// 注意:原函数已返回 Promise,不需要再 async 包一层
const execute = () => original.apply(notion, args)
const promise = useRateLimiter
? rateLimiter.enqueue(key, execute)
: execute()
: Promise.resolve().then(execute)

globalStore.inflight.set(key, promise)
promise.finally(() => globalStore.inflight.delete(key))
// 始终把 inflight 清掉;即便上层不消费 reject 也不抛 unhandledRejection
promise
.catch(() => {})
.finally(() => globalStore.inflight.delete(key))
return promise
}

Expand Down
6 changes: 4 additions & 2 deletions pages/[prefix]/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ const Slug = props => {
}
}
}
}, [post])
// validPassword 内部依赖 post / router 同时也已在依赖里
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [post, router.asPath])

// 文章加载
useEffect(() => {
Expand All @@ -89,7 +91,7 @@ const Slug = props => {
)
post.toc = getPageTableOfContents(post, post.blockMap)
}
}, [router, lock])
}, [router, lock, post])

props = { ...props, lock, validPassword }
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
Expand Down
2 changes: 1 addition & 1 deletion themes/heo/components/BlogPostCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {

return (
<article
className={` ${COVER_HOVER_ENLARGE} ? ' hover:transition-all duration-150' : ''}`}>
className={`${COVER_HOVER_ENLARGE ? 'hover:transition-all duration-150' : ''}`}>
<div
data-wow-delay='.2s'
className={
Expand Down
60 changes: 36 additions & 24 deletions themes/heo/components/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { siteConfig } from '@/lib/config'
import { isBrowser } from '@/lib/utils'
import throttle from 'lodash.throttle'
import { useRouter } from 'next/router'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import DarkModeButton from './DarkModeButton'
import Logo from './Logo'
import { MenuListTop } from './MenuListTop'
Expand All @@ -21,50 +21,62 @@ const Header = props => {
const [textWhite, setTextWhite] = useState(false)
const [navBgWhite, setBgWhite] = useState(false)
const [activeIndex, setActiveIndex] = useState(0)
// 是否存在文章页背景图(仅客户端检测)
const [hasPostBg, setHasPostBg] = useState(false)

const router = useRouter()
const slideOverRef = useRef()
// 缓存 #post-bg 节点的引用,避免每次滚动都重新查询 DOM
const postBgRef = useRef(null)

const toggleMenuOpen = () => {
slideOverRef?.current?.toggleSlideOvers()
}

/**
* 根据滚动条,切换导航栏样式
* 用 useMemo 持有 throttle 实例,避免每次渲染重建
*/
const scrollTrigger = useCallback(
throttle(() => {
const scrollS = window.scrollY
// 导航栏设置 白色背景
if (scrollS <= 1) {
setFixedNav(false)
setBgWhite(false)
setTextWhite(false)

// 文章详情页特殊处理
if (document?.querySelector('#post-bg')) {
const scrollTrigger = useMemo(
() =>
throttle(() => {
const scrollS = window.scrollY
// 导航栏设置 白色背景
if (scrollS <= 1) {
setFixedNav(false)
setBgWhite(false)
setTextWhite(false)

// 文章详情页特殊处理
if (postBgRef.current) {
setFixedNav(true)
setTextWhite(true)
}
} else {
// 向下滚动后的导航样式
setFixedNav(true)
setTextWhite(true)
setTextWhite(false)
setBgWhite(true)
}
} else {
// 向下滚动后的导航样式
setFixedNav(true)
setTextWhite(false)
setBgWhite(true)
}
}, 100)
}, 100),
[]
)

// 路由变化后重新探测 #post-bg 与初始化导航状态
useEffect(() => {
postBgRef.current = document.querySelector('#post-bg')
setHasPostBg(!!postBgRef.current)
scrollTrigger()
}, [router])
}, [router.asPath, scrollTrigger])

// 监听滚动
useEffect(() => {
window.addEventListener('scroll', scrollTrigger)
window.addEventListener('scroll', scrollTrigger, { passive: true })
return () => {
window.removeEventListener('scroll', scrollTrigger)
scrollTrigger.cancel()
}
}, [])
}, [scrollTrigger])

// 导航栏根据滚动轮播菜单内容
useEffect(() => {
Expand Down Expand Up @@ -133,7 +145,7 @@ const Header = props => {
`}</style>

{/* fixed时留白高度 */}
{fixedNav && !document?.querySelector('#post-bg') && (
{fixedNav && !hasPostBg && (
<div className='h-16'></div>
)}

Expand Down
10 changes: 7 additions & 3 deletions themes/heo/components/PostHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ export default function PostHeader({ post, siteInfo, isDarkMode }) {
return (
<div
id='post-bg'
className='md:mb-0 -mb-5 w-full h-[30rem] relative md:flex-shrink-0 overflow-hidden bg-cover bg-center bg-no-repeat z-10'>
className='md:mb-0 -mb-5 w-full h-[30rem] relative md:flex-shrink-0 overflow-hidden bg-cover bg-center bg-no-repeat z-10'
style={{
'--heo-post-bg-accent': isDarkMode ? '#CA8A04' : '#0060e0'
}}>
<style jsx>{`
.coverdiv:after {
position: absolute;
Expand All @@ -32,12 +35,13 @@ export default function PostHeader({ post, siteInfo, isDarkMode }) {
top: 0;
left: 0;
box-shadow: 110px -130px 500px 100px
${isDarkMode ? '#CA8A04' : '#0060e0'} inset;
var(--heo-post-bg-accent) inset;
}
`}</style>

<div
className={`${isDarkMode ? 'bg-[#CA8A04]' : 'bg-[#0060e0]'} absolute top-0 w-full h-full py-10 flex justify-center items-center`}>
className='absolute top-0 w-full h-full py-10 flex justify-center items-center'
style={{ backgroundColor: 'var(--heo-post-bg-accent)' }}>
{/* 文章背景图 */}
<div
id='post-cover-wrapper'
Expand Down
34 changes: 22 additions & 12 deletions themes/heo/components/PostLock.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useGlobal } from '@/lib/global'
import { useEffect, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'

/**
* 加密文章校验组件
Expand All @@ -11,20 +11,24 @@ import { useEffect, useRef } from 'react'
export const PostLock = props => {
const { validPassword } = props
const { locale } = useGlobal()
const [showError, setShowError] = useState(false)
const passwordInputRef = useRef(null)

const submitPassword = () => {
const p = document.getElementById('password')
if (!validPassword(p?.value)) {
const tips = document.getElementById('tips')
if (tips) {
tips.innerHTML = ''
tips.innerHTML = `<div class='text-red-500 animate__shakeX animate__animated'>${locale.COMMON.PASSWORD_ERROR}</div>`
}
const value = passwordInputRef.current?.value
if (!validPassword(value)) {
// 触发抖动动画:先取消再加上,让 CSS 动画重新跑
setShowError(false)
// 下一帧再设 true,确保动画重启
requestAnimationFrame(() => setShowError(true))
} else {
setShowError(false)
}
}
const passwordInputRef = useRef(null)

useEffect(() => {
// 选中密码输入框并将其聚焦
passwordInputRef.current.focus()
// 选中密码输入框并将其聚焦(带空保护,组件未挂载时不会崩)
passwordInputRef.current?.focus?.()
}, [])

return (
Expand Down Expand Up @@ -54,7 +58,13 @@ export const PostLock = props => {
</i>
</div>
</div>
<div id='tips'></div>
<div id='tips'>
{showError && (
<div className='text-red-500 animate__shakeX animate__animated'>
{locale.COMMON.PASSWORD_ERROR}
</div>
)}
</div>
</div>
</div>
)
Expand Down
10 changes: 6 additions & 4 deletions themes/heo/components/SearchInput.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { useRouter } from 'next/router'
import { useImperativeHandle, useRef, useState } from 'react'
import { useGlobal } from '@/lib/global'
let lock = false

const SearchInput = props => {
const { currentSearch, cRef, className } = props
const [onLoading, setLoadingState] = useState(false)
const router = useRouter()
const searchInputRef = useRef()
const { locale } = useGlobal()
// 输入法组合期锁定 — 用 useRef 持有,避免使用模块级变量在 SSR
// 多请求 / 多实例间互相污染(之前 `let lock = false` 是模块作用域)。
const lockRef = useRef(false)
useImperativeHandle(cRef, () => {
return {
focus: () => {
Expand Down Expand Up @@ -44,7 +46,7 @@ const SearchInput = props => {

const [showClean, setShowClean] = useState(false)
const updateSearchKey = val => {
if (lock) {
if (lockRef.current) {
return
}
searchInputRef.current.value = val
Expand All @@ -56,11 +58,11 @@ const SearchInput = props => {
}
}
function lockSearchInput () {
lock = true
lockRef.current = true
}

function unLockSearchInput () {
lock = false
lockRef.current = false
}

return (
Expand Down
Loading
Loading