Compare commits

...

4 Commits

Author SHA1 Message Date
5ab814d153 Add debouncing settle detection 2025-04-01 13:33:49 -04:00
fe83cd94ca Add debounce function 2025-04-01 13:33:49 -04:00
5178a72c52 Add registry for background promises 2025-04-01 13:33:49 -04:00
f4e801351c Add more TS lints (#6084)
* Fix to not call onMouseLeave with no selected object

* Add no this alias lint

* Add more lints and fix JSON formatting

* Fix to use lower-case string type

* Add another namespace lint

* Fix to not use plus on possibly non-string values
2025-04-01 10:21:31 -07:00
10 changed files with 163 additions and 18 deletions

View File

@ -26,11 +26,17 @@
"@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-duplicate-type-constituents": "error",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-for-in-array": "error",
"no-implied-eval": "off", // This is wrong; use the @typescript-eslint one instead.
"@typescript-eslint/no-implied-eval": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/no-redundant-type-constituents": "error",
"@typescript-eslint/no-this-alias": "warn",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unnecessary-type-constraint": "error",
"no-unused-vars": "off", // This is wrong; use the @typescript-eslint one instead.
@ -41,7 +47,13 @@
"vars": "all",
"args": "none"
}],
"@typescript-eslint/no-unsafe-unary-minus": "error",
"@typescript-eslint/no-wrapper-object-types": "error",
"no-throw-literal": "off", // Use @typescript-eslint/only-throw-error instead.
"@typescript-eslint/only-throw-error": "error",
"@typescript-eslint/prefer-as-const": "warn",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-autofocus": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
@ -50,7 +62,7 @@
{
"name": "isNaN",
"message": "Use Number.isNaN() instead."
},
}
],
"no-restricted-syntax": [
"error",
@ -64,7 +76,7 @@
"never"
],
"react-hooks/exhaustive-deps": "off",
"suggest-no-throw/suggest-no-throw": "warn",
"suggest-no-throw/suggest-no-throw": "error"
},
"overrides": [
{
@ -84,7 +96,7 @@
"plugin:testing-library/react"
],
"rules": {
"suggest-no-throw/suggest-no-throw": "off",
"suggest-no-throw/suggest-no-throw": "off"
}
}
]

View File

@ -124,6 +124,7 @@ export class ElectronZoo {
// We need to expose this in order for some tests that require folder
// creation and some code below.
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
const options = {

View File

@ -37,11 +37,11 @@ export class IntoServer
implements AsyncGenerator<Uint8Array, never, void>
{
private worker: Worker | null = null
private type_: String | null = null
private type_: string | null = null
private trace: boolean = false
constructor(type_?: String, worker?: Worker, trace?: boolean) {
constructor(type_?: string, worker?: Worker, trace?: boolean) {
super()
if (worker && type_) {
this.worker = worker

View File

@ -261,7 +261,7 @@ export class SceneInfra {
return null
}
hoveredObject: null | any = null
hoveredObject: null | Object3D<Object3DEventMap> = null
raycaster = new Raycaster()
planeRaycaster = new Raycaster()
currentMouseVector = new Vector2()
@ -487,11 +487,13 @@ export class SceneInfra {
if (this.hoveredObject !== firstIntersectObject) {
const hoveredObj = this.hoveredObject
this.hoveredObject = null
await this.onMouseLeave({
selected: hoveredObj,
mouseEvent: mouseEvent,
intersectionPoint,
})
if (hoveredObj) {
await this.onMouseLeave({
selected: hoveredObj,
mouseEvent: mouseEvent,
intersectionPoint,
})
}
this.hoveredObject = firstIntersectObject
await this.onMouseEnter({
selected: this.hoveredObject,

View File

@ -175,8 +175,11 @@ export const FileMachineProvider = ({
commandBarActor.send({ type: 'Close' })
navigate(
`..${PATHS.FILE}/${encodeURIComponent(
// TODO: Should this be context.selectedDirectory.path?
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
context.selectedDirectory +
window.electron.path.sep +
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
event.output.name
)}`
)

View File

@ -591,6 +591,7 @@ class EngineConnection extends EventTarget {
* did not establish.
*/
connect(reconnecting?: boolean): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
return new Promise((resolve) => {
if (this.isConnecting() || this.isReady()) {

View File

@ -274,11 +274,17 @@ export const parse = (code: string | Error): ParseResult | Error => {
}
}
// Parse and throw an exception if there are any errors (probably not suitable for use outside of testing).
export const assertParse = (code: string): Node<Program> => {
/**
* Parse and throw an exception if there are any errors (probably not suitable for use outside of testing).
*/
export function assertParse(code: string): Node<Program> {
const result = parse(code)
// eslint-disable-next-line suggest-no-throw/suggest-no-throw
if (err(result) || !resultIsOk(result)) throw result
if (err(result)) throw result
if (!resultIsOk(result)) {
// eslint-disable-next-line suggest-no-throw/suggest-no-throw
throw new Error('parse result contains errors', { cause: result })
}
return result.program
}
@ -565,8 +571,8 @@ export function base64Decode(base64: string): ArrayBuffer | Error {
const decoded = base64_decode(base64)
return new Uint8Array(decoded).buffer
} catch (e) {
console.error('Caught error decoding base64 string: ' + e)
return new Error('Caught error decoding base64 string: ' + e)
console.error('Caught error decoding base64 string', e)
return new Error('Caught error decoding base64 string', { cause: e })
}
}

97
src/lib/settle.ts Normal file
View File

@ -0,0 +1,97 @@
import { debounce } from './utils'
/**
* A registry for tracking background work and reacting once all work has
* settled.
*/
export class PromiseRegistry {
outstanding: Array<TrackedPromise<unknown>>
settleCallbacks: Array<() => void>
/**
* This reduces overhead when there are many promises all settling around the
* same time by trading some latency for when the overall settling is
* detected.
*/
private debouncedCleanUp: () => void
constructor() {
this.outstanding = []
this.settleCallbacks = []
this.debouncedCleanUp = debounce(this.attemptCleanUp.bind(this), 10)
}
track<T>(promise: Promise<T>, onSettle?: () => void) {
// Since built-in Promises don't have a way to synchronously check if
// they're settled, it cannot start out settled.
this.outstanding.push(new TrackedPromise(promise, this.debouncedCleanUp))
if (onSettle) {
this.settleCallbacks.push(onSettle)
}
}
/**
* Returns a promise that resolves when all promises have settled.
*/
waitForSettle(): Promise<void> {
return new Promise((resolve) => {
this.addSettleCallback(resolve)
})
}
/**
* Add a callback to be called when all promises have settled.
*
* @see waitForSettle for a Promise-based interface.
*/
addSettleCallback(onSettle: () => void) {
if (this.isSettled()) {
// Already settled, so schedule the callback.
setTimeout(onSettle, 0)
} else {
this.settleCallbacks.push(onSettle)
}
}
isSettled(): boolean {
return this.outstanding.every((p) => p.settled)
}
private attemptCleanUp() {
if (this.outstanding.length === 0) {
return
}
// Garbage collect.
const unsettled = this.outstanding.filter((p) => !p.settled)
this.outstanding = unsettled
// Transition to settled. We could move this into TrackedPromise.
// It's a trade-off between reducing latency and reducing overhead.
if (unsettled.length === 0) {
for (const cb of this.settleCallbacks) {
cb()
}
this.settleCallbacks = []
}
}
}
/**
* Native Promises don't have a way to synchronously detect if they're settled.
*/
class TrackedPromise<T> {
settled: boolean
inner: Promise<T>
constructor(promise: Promise<T>, onSettle: () => void) {
this.settled = false
this.inner = promise.finally(() => {
this.settled = true
onSettle()
})
}
}
/**
* Singleton regsitry for the whole app.
*/
export const AppPromises = new PromiseRegistry()

View File

@ -88,6 +88,29 @@ export function normaliseAngle(angle: number): number {
return result > 180 ? result - 360 : result
}
/**
* Returns a function that will delay the execution of the given function each
* time it's called until the wait time has passed.
*/
export function debounce<
F extends (...args: any[]) => void,
P extends Parameters<F>
>(func: F, wait: number): (...args: P) => void {
let timeout: ReturnType<typeof setTimeout> | null = null
function debounced(...args: P) {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
timeout = null
func(args)
}, wait)
}
return debounced
}
export function throttle<T>(
func: (args: T) => any,
wait: number

View File

@ -11,7 +11,7 @@ import type { WebContentSendPayload } from './menu/channels'
const typeSafeIpcRendererOn = (
channel: Channel,
listener: (event: IpcRendererEvent, ...args: any[]) => Promise<void> | any
listener: (event: IpcRendererEvent, ...args: any[]) => void
) => ipcRenderer.on(channel, listener)
const resizeWindow = (width: number, height: number) =>
@ -163,7 +163,7 @@ const listMachines = async (
})
}
const getMachineApiIp = async (): Promise<String | null> =>
const getMachineApiIp = async (): Promise<string | null> =>
ipcRenderer.invoke('find_machine_api')
const getArgvParsed = () => {