editor: register timeouts/intervals/rafs for disposal (#3852)
We have a lot of events that fire in the editor and, technically, they can fire after the Editor is long gone. This adds a registry/manager to track those timeout/interval/raf IDs (and some eslint rules to enforce it). Some other cleanups: - `requestAnimationFrame.polyfill.ts` looks like it's unused now (it used to be used in a prev. revision) - @ds300 I could use your feedback on the `EffectScheduler` tweak. in `useReactor` we do: `() => new EffectScheduler(name, reactFn, { scheduleEffect: (cb) => requestAnimationFrame(cb) }),` and that looks like it doesn't currently get disposed of properly. thoughts? happy to do that separately from this PR if you think that's a trickier thing. ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [ ] `feature` — New feature - [x] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Test async operations and make sure they don't fire after disposal. ### Release Notes - Editor: add registry of timeouts/intervals/rafs --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
23cf8729bc
commit
aadc0aab4d
35 changed files with 185 additions and 76 deletions
45
.eslintrc.js
45
.eslintrc.js
|
@ -85,6 +85,44 @@ module.exports = {
|
||||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['packages/editor/**/*', 'packages/tldraw/**/*'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-globals': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
name: 'setInterval',
|
||||||
|
message: 'Use the timers from @tldraw/util instead.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'setTimeout',
|
||||||
|
message: 'Use the timers from @tldraw/util instead.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'requestAnimationFrame',
|
||||||
|
message: 'Use the timers from @tldraw/util instead.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-restricted-properties': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
object: 'window',
|
||||||
|
property: 'setTimeout',
|
||||||
|
message: 'Use the timers from @tldraw/util instead.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
object: 'window',
|
||||||
|
property: 'setInterval',
|
||||||
|
message: 'Use the timers from @tldraw/util instead.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
object: 'window',
|
||||||
|
property: 'requestAnimationFrame',
|
||||||
|
message: 'Use the timers from @tldraw/util instead.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: ['e2e/**/*'],
|
files: ['e2e/**/*'],
|
||||||
rules: {
|
rules: {
|
||||||
|
@ -97,6 +135,13 @@ module.exports = {
|
||||||
'import/no-extraneous-dependencies': 'off',
|
'import/no-extraneous-dependencies': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['*.test.ts', '*.spec.ts'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-properties': 'off',
|
||||||
|
'no-restricted-globals': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: ['apps/examples/**/*'],
|
files: ['apps/examples/**/*'],
|
||||||
rules: {
|
rules: {
|
||||||
|
|
|
@ -19,10 +19,9 @@ export function CustomRenderer() {
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d')!
|
const ctx = canvas.getContext('2d')!
|
||||||
|
|
||||||
let isCancelled = false
|
let raf = -1
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
if (isCancelled) return
|
|
||||||
if (!canvas) return
|
if (!canvas) return
|
||||||
|
|
||||||
ctx.resetTransform()
|
ctx.resetTransform()
|
||||||
|
@ -94,13 +93,13 @@ export function CustomRenderer() {
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(render)
|
raf = requestAnimationFrame(render)
|
||||||
}
|
}
|
||||||
|
|
||||||
render()
|
render()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isCancelled = true
|
cancelAnimationFrame(raf)
|
||||||
}
|
}
|
||||||
}, [editor])
|
}, [editor])
|
||||||
|
|
||||||
|
|
|
@ -80,15 +80,15 @@ export default function UserPresenceExample() {
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
rRaf.current = requestAnimationFrame(loop)
|
rRaf.current = editor.timers.requestAnimationFrame(loop)
|
||||||
}
|
}
|
||||||
|
|
||||||
rRaf.current = requestAnimationFrame(loop)
|
rRaf.current = editor.timers.requestAnimationFrame(loop)
|
||||||
} else {
|
} else {
|
||||||
editor.store.mergeRemoteChanges(() => {
|
editor.store.mergeRemoteChanges(() => {
|
||||||
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
||||||
})
|
})
|
||||||
rRaf.current = setInterval(() => {
|
rRaf.current = editor.timers.setInterval(() => {
|
||||||
editor.store.mergeRemoteChanges(() => {
|
editor.store.mergeRemoteChanges(() => {
|
||||||
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
||||||
})
|
})
|
||||||
|
|
|
@ -40,6 +40,7 @@ import { StoreSchema } from '@tldraw/store';
|
||||||
import { StoreSideEffects } from '@tldraw/store';
|
import { StoreSideEffects } from '@tldraw/store';
|
||||||
import { StyleProp } from '@tldraw/tlschema';
|
import { StyleProp } from '@tldraw/tlschema';
|
||||||
import { StylePropValue } from '@tldraw/tlschema';
|
import { StylePropValue } from '@tldraw/tlschema';
|
||||||
|
import { Timers } from '@tldraw/utils';
|
||||||
import { TLAsset } from '@tldraw/tlschema';
|
import { TLAsset } from '@tldraw/tlschema';
|
||||||
import { TLAssetId } from '@tldraw/tlschema';
|
import { TLAssetId } from '@tldraw/tlschema';
|
||||||
import { TLAssetPartial } from '@tldraw/tlschema';
|
import { TLAssetPartial } from '@tldraw/tlschema';
|
||||||
|
@ -1107,6 +1108,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
[key: string]: Map<StyleProp<any>, string>;
|
[key: string]: Map<StyleProp<any>, string>;
|
||||||
};
|
};
|
||||||
readonly textMeasure: TextManager;
|
readonly textMeasure: TextManager;
|
||||||
|
readonly timers: Timers;
|
||||||
toggleLock(shapes: TLShape[] | TLShapeId[]): this;
|
toggleLock(shapes: TLShape[] | TLShapeId[]): this;
|
||||||
undo(): this;
|
undo(): this;
|
||||||
ungroupShapes(ids: TLShapeId[], options?: Partial<{
|
ungroupShapes(ids: TLShapeId[], options?: Partial<{
|
||||||
|
|
|
@ -165,7 +165,7 @@ function useCollaboratorState(editor: Editor, latestPresence: TLInstancePresence
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = editor.timers.setInterval(() => {
|
||||||
setState(getStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current))
|
setState(getStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current))
|
||||||
}, editor.options.collaboratorCheckIntervalMs)
|
}, editor.options.collaboratorCheckIntervalMs)
|
||||||
|
|
||||||
|
|
|
@ -82,12 +82,12 @@ export const DefaultErrorFallback: TLErrorFallbackComponent = ({ error, editor }
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (didCopy) {
|
if (didCopy) {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = editor?.timers.setTimeout(() => {
|
||||||
setDidCopy(false)
|
setDidCopy(false)
|
||||||
}, 2000)
|
}, 2000)
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout)
|
||||||
}
|
}
|
||||||
}, [didCopy])
|
}, [didCopy, editor])
|
||||||
|
|
||||||
const copyError = () => {
|
const copyError = () => {
|
||||||
const textarea = document.createElement('textarea')
|
const textarea = document.createElement('textarea')
|
||||||
|
|
|
@ -57,6 +57,7 @@ import {
|
||||||
JsonObject,
|
JsonObject,
|
||||||
PerformanceTracker,
|
PerformanceTracker,
|
||||||
Result,
|
Result,
|
||||||
|
Timers,
|
||||||
annotateError,
|
annotateError,
|
||||||
assert,
|
assert,
|
||||||
assertExists,
|
assertExists,
|
||||||
|
@ -240,6 +241,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
this.snaps = new SnapManager(this)
|
this.snaps = new SnapManager(this)
|
||||||
|
|
||||||
|
this.timers = new Timers()
|
||||||
|
this.disposables.add(this.timers.dispose.bind(this.timers))
|
||||||
|
|
||||||
this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions })
|
this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions })
|
||||||
|
|
||||||
this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false)
|
this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false)
|
||||||
|
@ -687,7 +691,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
this.on('tick', this._flushEventsForTick)
|
this.on('tick', this._flushEventsForTick)
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
this.timers.requestAnimationFrame(() => {
|
||||||
this._tickManager.start()
|
this._tickManager.start()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -727,6 +731,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
*/
|
*/
|
||||||
readonly snaps: SnapManager
|
readonly snaps: SnapManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A manager for the any asynchronous events and making sure they're
|
||||||
|
* cleaned up upon disposal.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
readonly timers: Timers
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A manager for the user and their preferences.
|
* A manager for the user and their preferences.
|
||||||
*
|
*
|
||||||
|
@ -1246,7 +1258,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
clearTimeout(this._isChangingStyleTimeout)
|
clearTimeout(this._isChangingStyleTimeout)
|
||||||
if (partial.isChangingStyle === true) {
|
if (partial.isChangingStyle === true) {
|
||||||
// If we've set to true, set a new reset timeout to change the value back to false after 2 seconds
|
// If we've set to true, set a new reset timeout to change the value back to false after 2 seconds
|
||||||
this._isChangingStyleTimeout = setTimeout(() => {
|
this._isChangingStyleTimeout = this.timers.setTimeout(() => {
|
||||||
this._updateInstanceState({ isChangingStyle: false }, { history: 'ignore' })
|
this._updateInstanceState({ isChangingStyle: false }, { history: 'ignore' })
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
@ -2958,7 +2970,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
this.updateInstanceState({ highlightedUserIds: [...highlightedUserIds, userId] })
|
this.updateInstanceState({ highlightedUserIds: [...highlightedUserIds, userId] })
|
||||||
|
|
||||||
// Unhighlight the user's cursor after a few seconds
|
// Unhighlight the user's cursor after a few seconds
|
||||||
setTimeout(() => {
|
this.timers.setTimeout(() => {
|
||||||
const highlightedUserIds = [...this.getInstanceState().highlightedUserIds]
|
const highlightedUserIds = [...this.getInstanceState().highlightedUserIds]
|
||||||
const index = highlightedUserIds.indexOf(userId)
|
const index = highlightedUserIds.indexOf(userId)
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
|
@ -8440,27 +8452,27 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.shiftKey) {
|
if (info.shiftKey) {
|
||||||
clearInterval(this._shiftKeyTimeout)
|
clearTimeout(this._shiftKeyTimeout)
|
||||||
this._shiftKeyTimeout = -1
|
this._shiftKeyTimeout = -1
|
||||||
inputs.shiftKey = true
|
inputs.shiftKey = true
|
||||||
} else if (!info.shiftKey && inputs.shiftKey && this._shiftKeyTimeout === -1) {
|
} else if (!info.shiftKey && inputs.shiftKey && this._shiftKeyTimeout === -1) {
|
||||||
this._shiftKeyTimeout = setTimeout(this._setShiftKeyTimeout, 150)
|
this._shiftKeyTimeout = this.timers.setTimeout(this._setShiftKeyTimeout, 150)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.altKey) {
|
if (info.altKey) {
|
||||||
clearInterval(this._altKeyTimeout)
|
clearTimeout(this._altKeyTimeout)
|
||||||
this._altKeyTimeout = -1
|
this._altKeyTimeout = -1
|
||||||
inputs.altKey = true
|
inputs.altKey = true
|
||||||
} else if (!info.altKey && inputs.altKey && this._altKeyTimeout === -1) {
|
} else if (!info.altKey && inputs.altKey && this._altKeyTimeout === -1) {
|
||||||
this._altKeyTimeout = setTimeout(this._setAltKeyTimeout, 150)
|
this._altKeyTimeout = this.timers.setTimeout(this._setAltKeyTimeout, 150)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.ctrlKey) {
|
if (info.ctrlKey) {
|
||||||
clearInterval(this._ctrlKeyTimeout)
|
clearTimeout(this._ctrlKeyTimeout)
|
||||||
this._ctrlKeyTimeout = -1
|
this._ctrlKeyTimeout = -1
|
||||||
inputs.ctrlKey = true
|
inputs.ctrlKey = true
|
||||||
} else if (!info.ctrlKey && inputs.ctrlKey && this._ctrlKeyTimeout === -1) {
|
} else if (!info.ctrlKey && inputs.ctrlKey && this._ctrlKeyTimeout === -1) {
|
||||||
this._ctrlKeyTimeout = setTimeout(this._setCtrlKeyTimeout, 150)
|
this._ctrlKeyTimeout = this.timers.setTimeout(this._setCtrlKeyTimeout, 150)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { originPagePoint, currentPagePoint } = inputs
|
const { originPagePoint, currentPagePoint } = inputs
|
||||||
|
@ -8645,7 +8657,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
if (!this.inputs.isPanning) {
|
if (!this.inputs.isPanning) {
|
||||||
// Start a long press timeout
|
// Start a long press timeout
|
||||||
this._longPressTimeout = setTimeout(() => {
|
this._longPressTimeout = this.timers.setTimeout(() => {
|
||||||
this.dispatch({
|
this.dispatch({
|
||||||
...info,
|
...info,
|
||||||
point: this.inputs.currentScreenPoint,
|
point: this.inputs.currentScreenPoint,
|
||||||
|
@ -8879,7 +8891,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
} else {
|
} else {
|
||||||
this.performanceTracker.start(name)
|
this.performanceTracker.start(name)
|
||||||
}
|
}
|
||||||
this.performanceTrackerTimeout = setTimeout(() => {
|
this.performanceTrackerTimeout = this.timers.setTimeout(() => {
|
||||||
this.performanceTracker.stop()
|
this.performanceTracker.stop()
|
||||||
}, 50)
|
}, 50)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export class ClickManager {
|
||||||
private _getClickTimeout = (state: TLClickState, id = uniqueId()) => {
|
private _getClickTimeout = (state: TLClickState, id = uniqueId()) => {
|
||||||
this._clickId = id
|
this._clickId = id
|
||||||
clearTimeout(this._clickTimeout)
|
clearTimeout(this._clickTimeout)
|
||||||
this._clickTimeout = setTimeout(
|
this._clickTimeout = this.editor.timers.setTimeout(
|
||||||
() => {
|
() => {
|
||||||
if (this._clickState === state && this._clickId === id) {
|
if (this._clickState === state && this._clickId === id) {
|
||||||
switch (this._clickState) {
|
switch (this._clickState) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ const throttleToNextFrame =
|
||||||
? // At test time we should use actual raf and not throttle, because throttle was set up to evaluate immediately during tests, which causes stack overflow
|
? // At test time we should use actual raf and not throttle, because throttle was set up to evaluate immediately during tests, which causes stack overflow
|
||||||
// for the tick manager since it sets up a raf loop.
|
// for the tick manager since it sets up a raf loop.
|
||||||
function mockThrottle(cb: any) {
|
function mockThrottle(cb: any) {
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
const frame = requestAnimationFrame(cb)
|
const frame = requestAnimationFrame(cb)
|
||||||
return () => cancelAnimationFrame(frame)
|
return () => cancelAnimationFrame(frame)
|
||||||
}
|
}
|
||||||
|
|
|
@ -273,7 +273,7 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
|
||||||
|
|
||||||
pinchState = 'not sure'
|
pinchState = 'not sure'
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
editor.timers.requestAnimationFrame(() => {
|
||||||
editor.dispatch({
|
editor.dispatch({
|
||||||
type: 'pinch',
|
type: 'pinch',
|
||||||
name: 'pinch_end',
|
name: 'pinch_end',
|
||||||
|
|
|
@ -37,7 +37,7 @@ export function useScreenBounds(ref: React.RefObject<HTMLElement>) {
|
||||||
|
|
||||||
// Rather than running getClientRects on every frame, we'll
|
// Rather than running getClientRects on every frame, we'll
|
||||||
// run it once a second or when the window resizes.
|
// run it once a second or when the window resizes.
|
||||||
const interval = setInterval(updateBounds, 1000)
|
const interval = editor.timers.setInterval(updateBounds, 1000)
|
||||||
window.addEventListener('resize', updateBounds)
|
window.addEventListener('resize', updateBounds)
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
|
|
@ -266,6 +266,7 @@ export class TLLocalSyncClient {
|
||||||
|
|
||||||
private isPersisting = false
|
private isPersisting = false
|
||||||
private didLastWriteError = false
|
private didLastWriteError = false
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
private scheduledPersistTimeout: ReturnType<typeof setTimeout> | null = null
|
private scheduledPersistTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -277,6 +278,7 @@ export class TLLocalSyncClient {
|
||||||
private schedulePersist() {
|
private schedulePersist() {
|
||||||
this.debug('schedulePersist', this.scheduledPersistTimeout)
|
this.debug('schedulePersist', this.scheduledPersistTimeout)
|
||||||
if (this.scheduledPersistTimeout) return
|
if (this.scheduledPersistTimeout) return
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
this.scheduledPersistTimeout = setTimeout(
|
this.scheduledPersistTimeout = setTimeout(
|
||||||
() => {
|
() => {
|
||||||
this.scheduledPersistTimeout = null
|
this.scheduledPersistTimeout = null
|
||||||
|
|
|
@ -35,7 +35,7 @@ interface EffectSchedulerOptions {
|
||||||
* @param execute - A function that will execute the effect.
|
* @param execute - A function that will execute the effect.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
scheduleEffect?: (execute: () => void) => void
|
scheduleEffect?: (execute: () => void) => number | void | (() => void)
|
||||||
}
|
}
|
||||||
|
|
||||||
class __EffectScheduler__<Result> {
|
class __EffectScheduler__<Result> {
|
||||||
|
@ -63,13 +63,15 @@ class __EffectScheduler__<Result> {
|
||||||
return this._scheduleCount
|
return this._scheduleCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
private maybeRaf: number | void | (() => void) = -1
|
||||||
/** @internal */
|
/** @internal */
|
||||||
readonly parentSet = new ArraySet<Signal<any, any>>()
|
readonly parentSet = new ArraySet<Signal<any, any>>()
|
||||||
/** @internal */
|
/** @internal */
|
||||||
readonly parentEpochs: number[] = []
|
readonly parentEpochs: number[] = []
|
||||||
/** @internal */
|
/** @internal */
|
||||||
readonly parents: Signal<any, any>[] = []
|
readonly parents: Signal<any, any>[] = []
|
||||||
private readonly _scheduleEffect?: (execute: () => void) => void
|
private readonly _scheduleEffect?: (execute: () => void) => number | void | (() => void)
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
private readonly runEffect: (lastReactedEpoch: number) => Result,
|
private readonly runEffect: (lastReactedEpoch: number) => Result,
|
||||||
|
@ -99,7 +101,7 @@ class __EffectScheduler__<Result> {
|
||||||
this._scheduleCount++
|
this._scheduleCount++
|
||||||
if (this._scheduleEffect) {
|
if (this._scheduleEffect) {
|
||||||
// if the effect should be deferred (e.g. until a react render), do so
|
// if the effect should be deferred (e.g. until a react render), do so
|
||||||
this._scheduleEffect(this.maybeExecute)
|
this.maybeRaf = this._scheduleEffect(this.maybeExecute)
|
||||||
} else {
|
} else {
|
||||||
// otherwise execute right now!
|
// otherwise execute right now!
|
||||||
this.execute()
|
this.execute()
|
||||||
|
@ -135,6 +137,7 @@ class __EffectScheduler__<Result> {
|
||||||
for (let i = 0, n = this.parents.length; i < n; i++) {
|
for (let i = 0, n = this.parents.length; i < n; i++) {
|
||||||
detach(this.parents[i], this)
|
detach(this.parents[i], this)
|
||||||
}
|
}
|
||||||
|
typeof this.maybeRaf === 'number' && cancelAnimationFrame(this.maybeRaf)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -841,7 +841,7 @@ export function getPerfectDashProps(totalLength: number, strokeWidth: number, op
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function getSvgAsImage(svgString: string, isSafari: boolean, options: {
|
export function getSvgAsImage(editor: Editor, svgString: string, options: {
|
||||||
height: number;
|
height: number;
|
||||||
quality: number;
|
quality: number;
|
||||||
scale: number;
|
scale: number;
|
||||||
|
|
|
@ -137,7 +137,7 @@ export const TldrawImage = memo(function TldrawImage(props: TldrawImageProps) {
|
||||||
setUrl(url)
|
setUrl(url)
|
||||||
}
|
}
|
||||||
} else if (format === 'png') {
|
} else if (format === 'png') {
|
||||||
const blob = await getSvgAsImage(svgResult.svg, editor.environment.isSafari, {
|
const blob = await getSvgAsImage(editor, svgResult.svg, {
|
||||||
type: format,
|
type: format,
|
||||||
quality: 1,
|
quality: 1,
|
||||||
scale: 2,
|
scale: 2,
|
||||||
|
|
|
@ -170,7 +170,7 @@ export class Pointing extends StateNode {
|
||||||
private preciseTimeout = -1
|
private preciseTimeout = -1
|
||||||
private didTimeout = false
|
private didTimeout = false
|
||||||
private startPreciseTimeout() {
|
private startPreciseTimeout() {
|
||||||
this.preciseTimeout = window.setTimeout(() => {
|
this.preciseTimeout = this.editor.timers.setTimeout(() => {
|
||||||
if (!this.getIsActive()) return
|
if (!this.getIsActive()) return
|
||||||
this.didTimeout = true
|
this.didTimeout = true
|
||||||
}, 320)
|
}, 320)
|
||||||
|
|
|
@ -280,11 +280,11 @@ function PatternFillDefForCanvas() {
|
||||||
const htmlLayer = findHtmlLayerParent(containerRef.current!)
|
const htmlLayer = findHtmlLayerParent(containerRef.current!)
|
||||||
if (htmlLayer) {
|
if (htmlLayer) {
|
||||||
// Wait for `patternContext` to be picked up
|
// Wait for `patternContext` to be picked up
|
||||||
requestAnimationFrame(() => {
|
editor.timers.requestAnimationFrame(() => {
|
||||||
htmlLayer.style.display = 'none'
|
htmlLayer.style.display = 'none'
|
||||||
|
|
||||||
// Wait for 'display = "none"' to take effect
|
// Wait for 'display = "none"' to take effect
|
||||||
requestAnimationFrame(() => {
|
editor.timers.requestAnimationFrame(() => {
|
||||||
htmlLayer.style.display = ''
|
htmlLayer.style.display = ''
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,7 +12,7 @@ export class DragAndDropManager {
|
||||||
|
|
||||||
prevDroppingShapeId: TLShapeId | null = null
|
prevDroppingShapeId: TLShapeId | null = null
|
||||||
|
|
||||||
droppingNodeTimer: ReturnType<typeof setTimeout> | null = null
|
droppingNodeTimer: number | null = null
|
||||||
|
|
||||||
first = true
|
first = true
|
||||||
|
|
||||||
|
@ -33,13 +33,13 @@ export class DragAndDropManager {
|
||||||
if (this.droppingNodeTimer === null) {
|
if (this.droppingNodeTimer === null) {
|
||||||
this.setDragTimer(movingShapes, INITIAL_POINTER_LAG_DURATION, cb)
|
this.setDragTimer(movingShapes, INITIAL_POINTER_LAG_DURATION, cb)
|
||||||
} else if (this.editor.inputs.pointerVelocity.len() > 0.5) {
|
} else if (this.editor.inputs.pointerVelocity.len() > 0.5) {
|
||||||
clearInterval(this.droppingNodeTimer)
|
clearTimeout(this.droppingNodeTimer)
|
||||||
this.setDragTimer(movingShapes, FAST_POINTER_LAG_DURATION, cb)
|
this.setDragTimer(movingShapes, FAST_POINTER_LAG_DURATION, cb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setDragTimer(movingShapes: TLShape[], duration: number, cb: () => void) {
|
private setDragTimer(movingShapes: TLShape[], duration: number, cb: () => void) {
|
||||||
this.droppingNodeTimer = setTimeout(() => {
|
this.droppingNodeTimer = this.editor.timers.setTimeout(() => {
|
||||||
this.editor.batch(() => {
|
this.editor.batch(() => {
|
||||||
this.handleDrag(this.editor.inputs.currentPagePoint, movingShapes, cb)
|
this.handleDrag(this.editor.inputs.currentPagePoint, movingShapes, cb)
|
||||||
})
|
})
|
||||||
|
@ -129,7 +129,7 @@ export class DragAndDropManager {
|
||||||
this.prevDroppingShapeId = null
|
this.prevDroppingShapeId = null
|
||||||
|
|
||||||
if (this.droppingNodeTimer !== null) {
|
if (this.droppingNodeTimer !== null) {
|
||||||
clearInterval(this.droppingNodeTimer)
|
clearTimeout(this.droppingNodeTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.droppingNodeTimer = null
|
this.droppingNodeTimer = null
|
||||||
|
|
|
@ -145,7 +145,7 @@ export class DraggingHandle extends StateNode {
|
||||||
this.clearExactTimeout()
|
this.clearExactTimeout()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.exactTimeout = setTimeout(() => {
|
this.exactTimeout = this.editor.timers.setTimeout(() => {
|
||||||
if (this.getIsActive() && !this.isPrecise) {
|
if (this.getIsActive() && !this.isPrecise) {
|
||||||
this.isPrecise = true
|
this.isPrecise = true
|
||||||
this.isPreciseId = this.pointingId
|
this.isPreciseId = this.pointingId
|
||||||
|
|
|
@ -54,6 +54,7 @@ const CurrentState = track(function CurrentState() {
|
||||||
})
|
})
|
||||||
|
|
||||||
function FPS() {
|
function FPS() {
|
||||||
|
const editor = useEditor()
|
||||||
const showFps = useValue('show_fps', () => debugFlags.showFps.get(), [debugFlags])
|
const showFps = useValue('show_fps', () => debugFlags.showFps.get(), [debugFlags])
|
||||||
|
|
||||||
const fpsRef = useRef<HTMLDivElement>(null)
|
const fpsRef = useRef<HTMLDivElement>(null)
|
||||||
|
@ -63,7 +64,7 @@ function FPS() {
|
||||||
|
|
||||||
const TICK_LENGTH = 250
|
const TICK_LENGTH = 250
|
||||||
let maxKnownFps = 0
|
let maxKnownFps = 0
|
||||||
let cancelled = false
|
let raf = -1
|
||||||
|
|
||||||
let start = performance.now()
|
let start = performance.now()
|
||||||
let currentTickLength = 0
|
let currentTickLength = 0
|
||||||
|
@ -78,8 +79,6 @@ function FPS() {
|
||||||
// of frames that we've seen since the last time we rendered,
|
// of frames that we've seen since the last time we rendered,
|
||||||
// and the actual time since the last render.
|
// and the actual time since the last render.
|
||||||
function loop() {
|
function loop() {
|
||||||
if (cancelled) return
|
|
||||||
|
|
||||||
// Count the frame
|
// Count the frame
|
||||||
framesInCurrentTick++
|
framesInCurrentTick++
|
||||||
|
|
||||||
|
@ -111,15 +110,15 @@ function FPS() {
|
||||||
start = performance.now()
|
start = performance.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(loop)
|
raf = editor.timers.requestAnimationFrame(loop)
|
||||||
}
|
}
|
||||||
|
|
||||||
loop()
|
loop()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelAnimationFrame(raf)
|
||||||
}
|
}
|
||||||
}, [showFps])
|
}, [showFps, editor])
|
||||||
|
|
||||||
if (!showFps) return null
|
if (!showFps) return null
|
||||||
|
|
||||||
|
|
|
@ -51,8 +51,8 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
|
||||||
const rInput = useRef<HTMLInputElement>(null)
|
const rInput = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
requestAnimationFrame(() => rInput.current?.focus())
|
editor.timers.requestAnimationFrame(() => rInput.current?.focus())
|
||||||
}, [])
|
}, [editor])
|
||||||
|
|
||||||
const rInitialValue = useRef(selectedShape.props.url)
|
const rInitialValue = useRef(selectedShape.props.url)
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,10 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
||||||
// has not yet entered a valid URL.
|
// has not yet entered a valid URL.
|
||||||
setShowError(false)
|
setShowError(false)
|
||||||
clearTimeout(rShowErrorTimeout.current)
|
clearTimeout(rShowErrorTimeout.current)
|
||||||
rShowErrorTimeout.current = setTimeout(() => setShowError(!embedInfo), 320)
|
rShowErrorTimeout.current = editor.timers.setTimeout(
|
||||||
|
() => setShowError(!embedInfo),
|
||||||
|
320
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{url === '' ? (
|
{url === '' ? (
|
||||||
|
|
|
@ -33,7 +33,7 @@ export function DefaultMinimap() {
|
||||||
origin: 'minimap',
|
origin: 'minimap',
|
||||||
willCrashApp: false,
|
willCrashApp: false,
|
||||||
})
|
})
|
||||||
setTimeout(() => {
|
editor.timers.setTimeout(() => {
|
||||||
throw e
|
throw e
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -188,11 +188,11 @@ export function DefaultMinimap() {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// need to wait a tick for next theme css to be applied
|
// need to wait a tick for next theme css to be applied
|
||||||
// otherwise the minimap will render with the wrong colors
|
// otherwise the minimap will render with the wrong colors
|
||||||
setTimeout(() => {
|
editor.timers.setTimeout(() => {
|
||||||
minimapRef.current?.updateColors()
|
minimapRef.current?.updateColors()
|
||||||
minimapRef.current?.render()
|
minimapRef.current?.render()
|
||||||
})
|
})
|
||||||
}, [isDarkMode])
|
}, [isDarkMode, editor])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tlui-minimap">
|
<div className="tlui-minimap">
|
||||||
|
|
|
@ -94,7 +94,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
|
||||||
// Scroll the current page into view when the menu opens / when current page changes
|
// Scroll the current page into view when the menu opens / when current page changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return
|
if (!isOpen) return
|
||||||
requestAnimationFrame(() => {
|
editor.timers.requestAnimationFrame(() => {
|
||||||
const elm = document.querySelector(
|
const elm = document.querySelector(
|
||||||
`[data-testid="page-menu-item-${currentPageId}"]`
|
`[data-testid="page-menu-item-${currentPageId}"]`
|
||||||
) as HTMLDivElement
|
) as HTMLDivElement
|
||||||
|
@ -118,7 +118,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [ITEM_HEIGHT, currentPageId, isOpen])
|
}, [ITEM_HEIGHT, currentPageId, isOpen, editor])
|
||||||
|
|
||||||
const handlePointerDown = useCallback(
|
const handlePointerDown = useCallback(
|
||||||
(e: React.PointerEvent<HTMLButtonElement>) => {
|
(e: React.PointerEvent<HTMLButtonElement>) => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as T from '@radix-ui/react-toast'
|
import * as T from '@radix-ui/react-toast'
|
||||||
|
import { useEditor } from '@tldraw/editor'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { AlertSeverity, TLUiToast, useToasts } from '../context/toasts'
|
import { AlertSeverity, TLUiToast, useToasts } from '../context/toasts'
|
||||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||||
|
@ -94,26 +95,25 @@ function _Toasts() {
|
||||||
export const Toasts = React.memo(_Toasts)
|
export const Toasts = React.memo(_Toasts)
|
||||||
|
|
||||||
export function ToastViewport() {
|
export function ToastViewport() {
|
||||||
|
const editor = useEditor()
|
||||||
const { toasts } = useToasts()
|
const { toasts } = useToasts()
|
||||||
|
|
||||||
const [hasToasts, setHasToasts] = React.useState(false)
|
const [hasToasts, setHasToasts] = React.useState(false)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let cancelled = false
|
let timeoutId = -1
|
||||||
if (toasts.length) {
|
if (toasts.length) {
|
||||||
setHasToasts(true)
|
setHasToasts(true)
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
timeoutId = editor.timers.setTimeout(() => {
|
||||||
if (!cancelled) {
|
|
||||||
setHasToasts(false)
|
setHasToasts(false)
|
||||||
}
|
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
clearTimeout(timeoutId)
|
||||||
}
|
}
|
||||||
}, [toasts.length, setHasToasts])
|
}, [toasts.length, setHasToasts, editor])
|
||||||
|
|
||||||
if (!hasToasts) return null
|
if (!hasToasts) return null
|
||||||
|
|
||||||
|
|
|
@ -74,14 +74,14 @@ export const TldrawUiInput = React.forwardRef<HTMLInputElement, TLUiInputProps>(
|
||||||
setIsFocused(true)
|
setIsFocused(true)
|
||||||
const elm = e.currentTarget as HTMLInputElement
|
const elm = e.currentTarget as HTMLInputElement
|
||||||
rCurrentValue.current = elm.value
|
rCurrentValue.current = elm.value
|
||||||
requestAnimationFrame(() => {
|
editor.timers.requestAnimationFrame(() => {
|
||||||
if (autoSelect) {
|
if (autoSelect) {
|
||||||
elm.select()
|
elm.select()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
onFocus?.()
|
onFocus?.()
|
||||||
},
|
},
|
||||||
[autoSelect, onFocus]
|
[autoSelect, onFocus, editor.timers]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleChange = React.useCallback(
|
const handleChange = React.useCallback(
|
||||||
|
@ -134,7 +134,7 @@ export const TldrawUiInput = React.forwardRef<HTMLInputElement, TLUiInputProps>(
|
||||||
visualViewport.addEventListener('resize', onViewportChange)
|
visualViewport.addEventListener('resize', onViewportChange)
|
||||||
visualViewport.addEventListener('scroll', onViewportChange)
|
visualViewport.addEventListener('scroll', onViewportChange)
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
editor.timers.requestAnimationFrame(() => {
|
||||||
rInputRef.current?.scrollIntoView({ block: 'center' })
|
rInputRef.current?.scrollIntoView({ block: 'center' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1216,7 +1216,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
// this needs to be deferred because it causes the menu
|
// this needs to be deferred because it causes the menu
|
||||||
// UI to unmount which puts us in a dodgy state
|
// UI to unmount which puts us in a dodgy state
|
||||||
requestAnimationFrame(() => {
|
editor.timers.requestAnimationFrame(() => {
|
||||||
editor.batch(() => {
|
editor.batch(() => {
|
||||||
trackEvent('toggle-focus-mode', { source })
|
trackEvent('toggle-focus-mode', { source })
|
||||||
clearDialogs()
|
clearDialogs()
|
||||||
|
|
|
@ -25,7 +25,7 @@ export function pasteTldrawContent(editor: Editor, clipboard: TLContent, point?:
|
||||||
) {
|
) {
|
||||||
// Creates a 'puff' to show a paste has happened.
|
// Creates a 'puff' to show a paste has happened.
|
||||||
editor.updateInstanceState({ isChangingStyle: true })
|
editor.updateInstanceState({ isChangingStyle: true })
|
||||||
setTimeout(() => {
|
editor.timers.setTimeout(() => {
|
||||||
editor.updateInstanceState({ isChangingStyle: false })
|
editor.updateInstanceState({ isChangingStyle: false })
|
||||||
}, 150)
|
}, 150)
|
||||||
}
|
}
|
||||||
|
|
|
@ -650,7 +650,7 @@ export function useNativeClipboardEvents() {
|
||||||
if (e.button === 1) {
|
if (e.button === 1) {
|
||||||
// middle mouse button
|
// middle mouse button
|
||||||
disablingMiddleClickPaste = true
|
disablingMiddleClickPaste = true
|
||||||
requestAnimationFrame(() => {
|
editor.timers.requestAnimationFrame(() => {
|
||||||
disablingMiddleClickPaste = false
|
disablingMiddleClickPaste = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,8 @@ import { TLExportType } from './exportAs'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export async function getSvgAsImage(
|
export async function getSvgAsImage(
|
||||||
|
editor: Editor,
|
||||||
svgString: string,
|
svgString: string,
|
||||||
isSafari: boolean,
|
|
||||||
options: {
|
options: {
|
||||||
type: 'png' | 'jpeg' | 'webp'
|
type: 'png' | 'jpeg' | 'webp'
|
||||||
quality: number
|
quality: number
|
||||||
|
@ -42,8 +42,8 @@ export async function getSvgAsImage(
|
||||||
// actually loaded. just waiting around a while is brittle, but
|
// actually loaded. just waiting around a while is brittle, but
|
||||||
// there doesn't seem to be any better solution for now :( see
|
// there doesn't seem to be any better solution for now :( see
|
||||||
// https://bugs.webkit.org/show_bug.cgi?id=219770
|
// https://bugs.webkit.org/show_bug.cgi?id=219770
|
||||||
if (isSafari) {
|
if (editor.environment.isSafari) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 250))
|
await new Promise((resolve) => editor.timers.setTimeout(resolve, 250))
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvas = document.createElement('canvas') as HTMLCanvasElement
|
const canvas = document.createElement('canvas') as HTMLCanvasElement
|
||||||
|
@ -157,7 +157,7 @@ export async function exportToBlob({
|
||||||
case 'webp': {
|
case 'webp': {
|
||||||
const svgResult = await getSvgString(editor, ids, opts)
|
const svgResult = await getSvgString(editor, ids, opts)
|
||||||
if (!svgResult) throw new Error('Could not construct image.')
|
if (!svgResult) throw new Error('Could not construct image.')
|
||||||
const image = await getSvgAsImage(svgResult.svg, editor.environment.isSafari, {
|
const image = await getSvgAsImage(editor, svgResult.svg, {
|
||||||
type: format,
|
type: format,
|
||||||
quality: 1,
|
quality: 1,
|
||||||
scale: 2,
|
scale: 2,
|
||||||
|
|
|
@ -20,7 +20,6 @@ import {
|
||||||
TLSocketServerSentEvent,
|
TLSocketServerSentEvent,
|
||||||
getTlsyncProtocolVersion,
|
getTlsyncProtocolVersion,
|
||||||
} from './protocol'
|
} from './protocol'
|
||||||
import './requestAnimationFrame.polyfill'
|
|
||||||
|
|
||||||
type SubscribingFn<T> = (cb: (val: T) => void) => () => void
|
type SubscribingFn<T> = (cb: (val: T) => void) => () => void
|
||||||
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
globalThis.requestAnimationFrame =
|
|
||||||
globalThis.requestAnimationFrame ||
|
|
||||||
function requestAnimationFrame(cb) {
|
|
||||||
return setTimeout(cb, 1000 / 60)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {}
|
|
|
@ -363,6 +363,18 @@ export function throttle<T extends (...args: any) => any>(func: T, limit: number
|
||||||
// @internal
|
// @internal
|
||||||
export function throttleToNextFrame(fn: () => void): () => void;
|
export function throttleToNextFrame(fn: () => void): () => void;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export class Timers {
|
||||||
|
// (undocumented)
|
||||||
|
dispose(): void;
|
||||||
|
// (undocumented)
|
||||||
|
requestAnimationFrame(callback: FrameRequestCallback): number;
|
||||||
|
// (undocumented)
|
||||||
|
setInterval(handler: TimerHandler, timeout?: number, ...args: any[]): number;
|
||||||
|
// (undocumented)
|
||||||
|
setTimeout(handler: TimerHandler, timeout?: number, ...args: any[]): number;
|
||||||
|
}
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export function validateIndexKey(key: string): asserts key is IndexKey;
|
export function validateIndexKey(key: string): asserts key is IndexKey;
|
||||||
|
|
||||||
|
|
|
@ -70,6 +70,7 @@ export {
|
||||||
setInSessionStorage,
|
setInSessionStorage,
|
||||||
} from './lib/storage'
|
} from './lib/storage'
|
||||||
export { fpsThrottle, throttleToNextFrame } from './lib/throttle'
|
export { fpsThrottle, throttleToNextFrame } from './lib/throttle'
|
||||||
|
export { Timers } from './lib/timers'
|
||||||
export type { Expand, RecursivePartial, Required } from './lib/types'
|
export type { Expand, RecursivePartial, Required } from './lib/types'
|
||||||
export {
|
export {
|
||||||
STRUCTURED_CLONE_OBJECT_PROTOTYPE,
|
STRUCTURED_CLONE_OBJECT_PROTOTYPE,
|
||||||
|
|
38
packages/utils/src/lib/timers.ts
Normal file
38
packages/utils/src/lib/timers.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/** @public */
|
||||||
|
export class Timers {
|
||||||
|
private timeouts: number[] = []
|
||||||
|
private intervals: number[] = []
|
||||||
|
private rafs: number[] = []
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
setTimeout(handler: TimerHandler, timeout?: number, ...args: any[]): number {
|
||||||
|
const id = window.setTimeout(handler, timeout, args)
|
||||||
|
this.timeouts.push(id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
setInterval(handler: TimerHandler, timeout?: number, ...args: any[]): number {
|
||||||
|
const id = window.setInterval(handler, timeout, args)
|
||||||
|
this.intervals.push(id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
requestAnimationFrame(callback: FrameRequestCallback): number {
|
||||||
|
const id = window.requestAnimationFrame(callback)
|
||||||
|
this.rafs.push(id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
dispose() {
|
||||||
|
this.timeouts.forEach((id) => clearTimeout(id))
|
||||||
|
this.intervals.forEach((id) => clearInterval(id))
|
||||||
|
this.rafs.forEach((id) => cancelAnimationFrame(id))
|
||||||
|
|
||||||
|
this.timeouts.length = 0
|
||||||
|
this.intervals.length = 0
|
||||||
|
this.rafs.length = 0
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue