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())
|
||||
|
||||
Generated
+3
-9
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user