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())
+3 -9
View File
@@ -2922,7 +2922,7 @@ importers:
version: 66.5.11
'@wxt-dev/module-vue':
specifier: ^1.0.3
version: 1.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
version: 1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
vue-tsc:
specifier: ^3.1.8
version: 3.2.1(typescript@5.9.3)
@@ -22392,12 +22392,6 @@ snapshots:
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
vue: 3.5.25(typescript@5.9.3)
'@vitejs/plugin-vue@6.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.53
vite: 8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
vue: 3.5.25(typescript@5.9.3)
'@vitest/browser-playwright@4.0.16(bufferutil@4.1.0)(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)':
dependencies:
'@vitest/browser': 4.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)
@@ -23144,9 +23138,9 @@ snapshots:
'@types/filesystem': 0.0.36
'@types/har-format': 1.2.16
'@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
'@wxt-dev/module-vue@1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@vitejs/plugin-vue': 6.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
'@vitejs/plugin-vue': 6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
wxt: 0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies:
- vite