fix(stage-tamagotchi): consolidated renderer loop helper (#936)
This commit is contained in:
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user