From 92fdb7e99efb99291abe0ce0555bc578d135b2cf Mon Sep 17 00:00:00 2001 From: "danil.radkovskyi" Date: Tue, 16 Jul 2024 12:20:49 +0200 Subject: [PATCH 1/5] Use debounce instead of throttle to avoid unnecessary re-renders during the code modifications --- src/elements/play-pen/play-pen.ts | 4 ++-- src/utils/debounce.test.ts | 29 +++++++++++++++++++++++++++++ src/utils/debounce.ts | 13 +++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/utils/debounce.test.ts create mode 100644 src/utils/debounce.ts diff --git a/src/elements/play-pen/play-pen.ts b/src/elements/play-pen/play-pen.ts index 577b727..bf3e7f6 100644 --- a/src/elements/play-pen/play-pen.ts +++ b/src/elements/play-pen/play-pen.ts @@ -1,5 +1,4 @@ import type {Empty, LinkedBundle} from '@devvit/protos' -import {throttle} from '@devvit/shared-types/throttle.js' import type {DevvitUIError} from '@devvit/ui-renderer/client/devvit-custom-post.js' import type {VirtualTypeScriptEnvironment} from '@typescript/vfs' import { @@ -57,6 +56,7 @@ import { emptyAssetsState, PlayAssets } from '../play-assets/play-assets.js' +import {debounce} from '../../utils/debounce.js' declare global { interface HTMLElementTagNameMap { @@ -444,7 +444,7 @@ export class PlayPen extends LitElement { } /** Throttled changes after updating sources. */ - #setSrcSideEffects = throttle((save: boolean): void => { + #setSrcSideEffects = debounce((save: boolean): void => { this.#version++ this._bundle = link( compile(this.#env), diff --git a/src/utils/debounce.test.ts b/src/utils/debounce.test.ts new file mode 100644 index 0000000..dac9c78 --- /dev/null +++ b/src/utils/debounce.test.ts @@ -0,0 +1,29 @@ +import {debounce} from './debounce' +import {expect, it, describe} from 'vitest' + +describe('debounce', ()=> { + it('executes with a delay', async () => { + const out = {val: 0}; + const fn = debounce((val: number) => (out.val = val), 100); + fn(1); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(out.val).toBe(0); + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(out.val).toBe(1); + }); + it('executes the last call of a series', async () => { + const out = {val: 0}; + const fn = debounce((val: number) => (out.val = val), 100); + fn(1); + await new Promise((resolve) => setTimeout(resolve, 0)); + fn(2); + await new Promise((resolve) => setTimeout(resolve, 0)); + fn(3); + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(out.val).toBe(3); + + fn(4); + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(out.val).toBe(4); + }); +}); \ No newline at end of file diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts new file mode 100644 index 0000000..45b1ac3 --- /dev/null +++ b/src/utils/debounce.ts @@ -0,0 +1,13 @@ +/** Delay function execution until after the invocations stopped. */ +export const debounce = ( + fn: (...args: T) => void, + period: number +): (...args: T) => void => { + let timeout: ReturnType | undefined; + return (...args: T) => { + clearTimeout(timeout); + timeout = setTimeout(()=>{ + fn(...args) + }, period) + }; +} From 576e8dbc15284179c7780c89bdbf04d7f5311229 Mon Sep 17 00:00:00 2001 From: "danil.radkovskyi" Date: Tue, 16 Jul 2024 12:23:28 +0200 Subject: [PATCH 2/5] newline --- src/utils/debounce.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/debounce.test.ts b/src/utils/debounce.test.ts index dac9c78..06948a0 100644 --- a/src/utils/debounce.test.ts +++ b/src/utils/debounce.test.ts @@ -26,4 +26,4 @@ describe('debounce', ()=> { await new Promise((resolve) => setTimeout(resolve, 100)); expect(out.val).toBe(4); }); -}); \ No newline at end of file +}); From b4859c735e21f3b6324e954444deb07de749facf Mon Sep 17 00:00:00 2001 From: "danil.radkovskyi" Date: Tue, 16 Jul 2024 12:25:44 +0200 Subject: [PATCH 3/5] Fix format --- src/utils/debounce.test.ts | 46 +++++++++++++++++++------------------- src/utils/debounce.ts | 10 ++++----- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/utils/debounce.test.ts b/src/utils/debounce.test.ts index 06948a0..f7250fa 100644 --- a/src/utils/debounce.test.ts +++ b/src/utils/debounce.test.ts @@ -1,29 +1,29 @@ import {debounce} from './debounce' import {expect, it, describe} from 'vitest' -describe('debounce', ()=> { +describe('debounce', () => { it('executes with a delay', async () => { - const out = {val: 0}; - const fn = debounce((val: number) => (out.val = val), 100); - fn(1); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(out.val).toBe(0); - await new Promise((resolve) => setTimeout(resolve, 100)); - expect(out.val).toBe(1); - }); + const out = {val: 0} + const fn = debounce((val: number) => (out.val = val), 100) + fn(1) + await new Promise(resolve => setTimeout(resolve, 0)) + expect(out.val).toBe(0) + await new Promise(resolve => setTimeout(resolve, 100)) + expect(out.val).toBe(1) + }) it('executes the last call of a series', async () => { - const out = {val: 0}; - const fn = debounce((val: number) => (out.val = val), 100); - fn(1); - await new Promise((resolve) => setTimeout(resolve, 0)); - fn(2); - await new Promise((resolve) => setTimeout(resolve, 0)); - fn(3); - await new Promise((resolve) => setTimeout(resolve, 100)); - expect(out.val).toBe(3); + const out = {val: 0} + const fn = debounce((val: number) => (out.val = val), 100) + fn(1) + await new Promise(resolve => setTimeout(resolve, 0)) + fn(2) + await new Promise(resolve => setTimeout(resolve, 0)) + fn(3) + await new Promise(resolve => setTimeout(resolve, 100)) + expect(out.val).toBe(3) - fn(4); - await new Promise((resolve) => setTimeout(resolve, 100)); - expect(out.val).toBe(4); - }); -}); + fn(4) + await new Promise(resolve => setTimeout(resolve, 100)) + expect(out.val).toBe(4) + }) +}) diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts index 45b1ac3..ac9f86c 100644 --- a/src/utils/debounce.ts +++ b/src/utils/debounce.ts @@ -2,12 +2,12 @@ export const debounce = ( fn: (...args: T) => void, period: number -): (...args: T) => void => { - let timeout: ReturnType | undefined; +): ((...args: T) => void) => { + let timeout: ReturnType | undefined return (...args: T) => { - clearTimeout(timeout); - timeout = setTimeout(()=>{ + clearTimeout(timeout) + timeout = setTimeout(() => { fn(...args) }, period) - }; + } } From 792274f9be931a07301df0ec53c033ff16cf2e31 Mon Sep 17 00:00:00 2001 From: "danil.radkovskyi" Date: Tue, 16 Jul 2024 14:23:02 +0200 Subject: [PATCH 4/5] Fix file extension --- src/utils/debounce.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/debounce.test.ts b/src/utils/debounce.test.ts index f7250fa..be7f10d 100644 --- a/src/utils/debounce.test.ts +++ b/src/utils/debounce.test.ts @@ -1,4 +1,4 @@ -import {debounce} from './debounce' +import {debounce} from './debounce.js' import {expect, it, describe} from 'vitest' describe('debounce', () => { From d53d457e21450c10363e398b22499d219052df77 Mon Sep 17 00:00:00 2001 From: "danil.radkovskyi" Date: Wed, 17 Jul 2024 15:02:09 +0200 Subject: [PATCH 5/5] Add tests and a cleanup function --- src/elements/play-pen/play-pen.ts | 16 +++++++++++----- src/utils/debounce.test.ts | 18 +++++++++++++++++- src/utils/debounce.ts | 7 ++++++- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/elements/play-pen/play-pen.ts b/src/elements/play-pen/play-pen.ts index bf3e7f6..26c6dc5 100644 --- a/src/elements/play-pen/play-pen.ts +++ b/src/elements/play-pen/play-pen.ts @@ -56,7 +56,7 @@ import { emptyAssetsState, PlayAssets } from '../play-assets/play-assets.js' -import {debounce} from '../../utils/debounce.js' +import {debounce, type DebounceCleanupFn} from '../../utils/debounce.js' declare global { interface HTMLElementTagNameMap { @@ -74,9 +74,7 @@ declare global { export class PlayPen extends LitElement { static override readonly styles: CSSResultGroup = css` ${cssReset} - ${unsafeCSS(penVars)} - :host { /* Light mode. */ color: var(--color-foreground); @@ -118,6 +116,7 @@ export class PlayPen extends LitElement { } /* Makes dropdowns appear over other content */ + play-pen-header, play-pen-footer { z-index: var(--z-menu); @@ -181,6 +180,8 @@ export class PlayPen extends LitElement { private _src: string | undefined #template?: boolean + #cleanupDelayedSideEffects: DebounceCleanupFn = () => {} + override connectedCallback(): void { super.connectedCallback() @@ -218,6 +219,11 @@ export class PlayPen extends LitElement { this.#setName(pen.name, false) } + override disconnectedCallback() { + this.#cleanupDelayedSideEffects() + super.disconnectedCallback() + } + protected override render(): TemplateResult { return html` { this.#version++ this._bundle = link( diff --git a/src/utils/debounce.test.ts b/src/utils/debounce.test.ts index be7f10d..a7407fb 100644 --- a/src/utils/debounce.test.ts +++ b/src/utils/debounce.test.ts @@ -1,5 +1,5 @@ import {debounce} from './debounce.js' -import {expect, it, describe} from 'vitest' +import {expect, it, describe, vi} from 'vitest' describe('debounce', () => { it('executes with a delay', async () => { @@ -11,13 +11,18 @@ describe('debounce', () => { await new Promise(resolve => setTimeout(resolve, 100)) expect(out.val).toBe(1) }) + it('executes the last call of a series', async () => { const out = {val: 0} const fn = debounce((val: number) => (out.val = val), 100) fn(1) await new Promise(resolve => setTimeout(resolve, 0)) + expect(out.val).toBe(0) + fn(2) await new Promise(resolve => setTimeout(resolve, 0)) + expect(out.val).toBe(0) + fn(3) await new Promise(resolve => setTimeout(resolve, 100)) expect(out.val).toBe(3) @@ -26,4 +31,15 @@ describe('debounce', () => { await new Promise(resolve => setTimeout(resolve, 100)) expect(out.val).toBe(4) }) + + it('has a cleanup function that cancels the delayed execution', async () => { + const innerFn = vi.fn() + const debouncedFn = debounce(innerFn, 1000) + const cleanupFn = debouncedFn(1) + await new Promise(resolve => setTimeout(resolve, 0)) + expect(innerFn).toBeCalledTimes(0) + cleanupFn() + await new Promise(resolve => setTimeout(resolve, 1000)) + expect(innerFn).toBeCalledTimes(0) + }) }) diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts index ac9f86c..7ad5a49 100644 --- a/src/utils/debounce.ts +++ b/src/utils/debounce.ts @@ -1,13 +1,18 @@ /** Delay function execution until after the invocations stopped. */ +export type DebounceCleanupFn = () => void + export const debounce = ( fn: (...args: T) => void, period: number -): ((...args: T) => void) => { +): ((...args: T) => DebounceCleanupFn) => { let timeout: ReturnType | undefined return (...args: T) => { clearTimeout(timeout) timeout = setTimeout(() => { fn(...args) }, period) + return () => { + clearTimeout(timeout) + } } }