fix(stage-tamagotchi): consolidated renderer loop helper (#936)

This commit is contained in:
sed-i
2026-01-11 21:47:56 +08:00
committed by GitHub
parent 061def6436
commit f2c308ffa4
5 changed files with 141 additions and 53 deletions
@@ -0,0 +1,75 @@
import type { BrowserWindow } from 'electron'
import { attemptAsync } from 'es-toolkit'
import { useLoop } from '../event-loop'
const rendererDisposedMessage = 'Render frame was disposed before WebFrameMain could be accessed'
export function isRendererUnavailable(window: BrowserWindow) {
return window.isDestroyed() || window.webContents.isDestroyed() || window.webContents.isCrashed()
}
export function shouldStopForRendererError(error: unknown) {
if (!(error instanceof Error) || !error.message) {
return false
}
return error.message.includes(rendererDisposedMessage)
}
export function stopLoopWhenRendererIsGone(window: BrowserWindow, stop: () => void) {
window.on('closed', stop)
window.webContents.on('destroyed', stop)
window.webContents.on('render-process-gone', stop)
}
function ensureRendererIsAvailable(window: BrowserWindow, stop: () => void) {
if (isRendererUnavailable(window)) {
stop()
return false
}
return true
}
export function createRendererLoop(params: { window: BrowserWindow, run: () => Promise<void> | void, interval?: number, autoStart?: boolean }) {
const { start, stop } = useLoop(async () => {
if (!ensureRendererIsAvailable(params.window, stop)) {
return
}
const [error] = await attemptAsync(async () => {
await params.run()
})
if (!error) {
return
}
if (shouldStopForRendererError(error)) {
stop()
return
}
throw error
}, {
autoStart: params.autoStart ?? false,
interval: params.interval,
})
stopLoopWhenRendererIsGone(params.window, stop)
const startLoop = () => {
if (!ensureRendererIsAvailable(params.window, stop)) {
return
}
start()
}
return {
start: startLoop,
stop,
}
}
@@ -1,47 +1,68 @@
export function useLoop(fn: () => Promise<void> | void, options?: { interval?: number, autoStart?: boolean }) {
let timer: NodeJS.Timeout | null = null
import { clearClockInterval, setClockInterval } from '@moeru/std'
import { Mutex } from 'es-toolkit/promise'
interface LoopOptions {
interval?: number
autoStart?: boolean
}
export function useLoop(fn: () => Promise<void> | void, options?: LoopOptions) {
const mutex = new Mutex()
const interval = options?.interval ?? 1000 / 60
let timerId: number | null = null
let shouldRun = options?.autoStart ?? true
const loopIteration = async () => {
if (!shouldRun) {
const tick = async () => {
if (!shouldRun || mutex.isLocked) {
return
}
await mutex.acquire()
try {
await fn()
}
finally {
timer = setTimeout(loopIteration, options?.interval ?? 1000 / 60) // Default to ~60 FPS
mutex.release()
}
}
const startTimer = () => {
if (!shouldRun || timerId !== null) {
return
}
timerId = setClockInterval(() => {
void tick()
}, interval)
}
const stopTimer = () => {
if (timerId === null) {
return
}
clearClockInterval(timerId)
timerId = null
}
const toggleRunState = (next: boolean) => {
shouldRun = next
if (shouldRun) {
startTimer()
return
}
stopTimer()
}
if (shouldRun) {
loopIteration()
startTimer()
}
return {
start: () => {
shouldRun = true
if (!timer) {
loopIteration()
}
},
resume: () => {
shouldRun = true
if (!timer) {
loopIteration()
}
},
pause: () => {
shouldRun = false
},
stop: () => {
shouldRun = false
if (timer) {
clearTimeout(timer)
timer = null
}
},
start: () => toggleRunState(true),
resume: () => toggleRunState(true),
pause: () => toggleRunState(false),
stop: () => toggleRunState(false),
}
}
@@ -7,14 +7,15 @@ import { screen } from 'electron'
import { cursorScreenPoint, startLoopGetCursorScreenPoint } from '../../../shared/electron/screen'
import { electron } from '../../../shared/eventa'
import { onAppBeforeQuit, onAppWindowAllClosed } from '../../libs/bootkit/lifecycle'
import { useLoop } from '../../libs/event-loop'
import { createRendererLoop } from '../../libs/electron/renderer-loop'
export function createScreenService(params: { context: ReturnType<typeof createContext>['context'], window: BrowserWindow }) {
const { start, stop } = useLoop(() => {
const dipPos = screen.getCursorScreenPoint()
params.context.emit(cursorScreenPoint, dipPos)
}, {
autoStart: false,
const { start, stop } = createRendererLoop({
window: params.window,
run: () => {
const dipPos = screen.getCursorScreenPoint()
params.context.emit(cursorScreenPoint, dipPos)
},
})
onAppWindowAllClosed(() => stop())
@@ -6,18 +6,15 @@ import { defineInvokeHandler } from '@moeru/eventa'
import { bounds, startLoopGetBounds } from '../../../shared/electron/window'
import { electron } from '../../../shared/eventa'
import { onAppBeforeQuit, onAppWindowAllClosed } from '../../libs/bootkit/lifecycle'
import { useLoop } from '../../libs/event-loop'
import { createRendererLoop } from '../../libs/electron/renderer-loop'
import { resizeWindowByDelta } from '../../windows/shared/window'
export function createWindowService(params: { context: ReturnType<typeof createContext>['context'], window: BrowserWindow }) {
const { start, stop } = useLoop(() => {
if (params.window.isDestroyed()) {
return
}
params.context.emit(bounds, params.window.getBounds())
}, {
autoStart: false,
const { start, stop } = createRendererLoop({
window: params.window,
run: () => {
params.context.emit(bounds, params.window.getBounds())
},
})
onAppWindowAllClosed(() => stop())