import { LitElement, html } from 'lit'
import { styleMap } from 'lit/directives/style-map.js'
import { TanStackDevtoolsCore } from '@tanstack/devtools'
import { pacerDevtoolsPlugin } from '@tanstack/pacer-devtools'
import { rateLimit } from '@tanstack/lit-pacer/rate-limiter'
class Counter extends LitElement {
static properties = {
windowType: { state: true },
instantCount: { state: true },
rateLimitedCount: { state: true },
}
windowType: 'fixed' | 'sliding' = 'fixed'
instantCount = 0
rateLimitedCount = 0
private rateLimitedSetCountWindow = this.windowType
private rateLimitedSetCountFunction = rateLimit(
(value: typeof this.rateLimitedCount) => (this.rateLimitedCount = value),
{
limit: 5,
window: 5000,
windowType: this.windowType,
onReject: (rateLimiter) =>
console.log(
'Rejected by rate limiter',
rateLimiter.getMsUntilNextWindow(),
),
},
)
get rateLimitedSetCount() {
if (this.rateLimitedSetCountWindow !== this.windowType) {
this.rateLimitedSetCountWindow = this.windowType
this.rateLimitedSetCountFunction = rateLimit(
(value: typeof this.rateLimitedCount) =>
(this.rateLimitedCount = value),
{
limit: 5,
window: 5000,
windowType: this.windowType,
onReject: (rateLimiter) =>
console.log(
'Rejected by rate limiter',
rateLimiter.getMsUntilNextWindow(),
),
},
)
}
return this.rateLimitedSetCountFunction
}
increment = () => {
this.instantCount = ((c) => {
const newInstantCount = c + 1
this.rateLimitedSetCount(newInstantCount)
return newInstantCount
})(this.instantCount)
}
override createRenderRoot() {
return this
}
override render() {
return html`<div>
<h1>TanStack Pacer rateLimit Example 1</h1>
<div
style=${styleMap({ display: 'grid', gap: '0.5rem', marginBottom: '1rem' })}
>
<label
><input
type="radio"
name="windowType"
value="fixed"
.checked=${this.windowType === 'fixed'}
@input=${() => (this.windowType = 'fixed')}
/>Fixed Window</label
><label
><input
type="radio"
name="windowType"
value="sliding"
.checked=${this.windowType === 'sliding'}
@input=${() => (this.windowType = 'sliding')}
/>Sliding Window</label
>
</div>
<table>
<tbody>
<tr>
<td>Instant Count:</td>
<td>${this.instantCount}</td>
</tr>
<tr>
<td>Rate Limited Count:</td>
<td>${this.rateLimitedCount}</td>
</tr>
</tbody>
</table>
<div><button @click=${this.increment}>Increment</button></div>
</div>`
}
}
customElements.define('pacer-counter', Counter)
class Search extends LitElement {
static properties = {
windowType: { state: true },
text: { state: true },
rateLimitedText: { state: true },
}
windowType: 'fixed' | 'sliding' = 'fixed'
text = ''
rateLimitedText = ''
private rateLimitedSetTextWindow = this.windowType
private rateLimitedSetTextFunction = rateLimit(
(value: typeof this.rateLimitedText) => (this.rateLimitedText = value),
{
limit: 5,
window: 5000,
windowType: this.windowType,
onReject: (rateLimiter) =>
console.log(
'Rejected by rate limiter',
rateLimiter.getMsUntilNextWindow(),
),
},
)
get rateLimitedSetText() {
if (this.rateLimitedSetTextWindow !== this.windowType) {
this.rateLimitedSetTextWindow = this.windowType
this.rateLimitedSetTextFunction = rateLimit(
(value: typeof this.rateLimitedText) => (this.rateLimitedText = value),
{
limit: 5,
window: 5000,
windowType: this.windowType,
onReject: (rateLimiter) =>
console.log(
'Rejected by rate limiter',
rateLimiter.getMsUntilNextWindow(),
),
},
)
}
return this.rateLimitedSetTextFunction
}
handleTextChange = (e: Event) => {
const newValue = (e.target as HTMLInputElement).value
this.text = newValue
this.rateLimitedSetText(newValue)
}
override createRenderRoot() {
return this
}
override render() {
return html`<div>
<h1>TanStack Pacer rateLimit Example 2</h1>
<div
style=${styleMap({ display: 'grid', gap: '0.5rem', marginBottom: '1rem' })}
>
<label
><input
type="radio"
name="windowType2"
value="fixed"
.checked=${this.windowType === 'fixed'}
@input=${() => (this.windowType = 'fixed')}
/>Fixed Window</label
><label
><input
type="radio"
name="windowType2"
value="sliding"
.checked=${this.windowType === 'sliding'}
@input=${() => (this.windowType = 'sliding')}
/>Sliding Window</label
>
</div>
<div>
<input
type="search"
.value=${this.text}
@input=${this.handleTextChange}
placeholder="Type text (rate limited to 5 updates per 5 seconds)..."
style=${styleMap({ width: '100%' })}
/>
</div>
<table>
<tbody>
<tr>
<td>Instant Text:</td>
<td>${this.text}</td>
</tr>
<tr>
<td>Rate Limited Text:</td>
<td>${this.rateLimitedText}</td>
</tr>
</tbody>
</table>
</div>`
}
}
customElements.define('pacer-search', Search)
class Range extends LitElement {
static properties = {
windowType: { state: true },
currentValue: { state: true },
rateLimitedValue: { state: true },
}
windowType: 'fixed' | 'sliding' = 'fixed'
currentValue = 50
rateLimitedValue = 50
private rateLimitedSetValueWindow = this.windowType
private rateLimitedSetValueFunction = rateLimit(
(value: typeof this.rateLimitedValue) => (this.rateLimitedValue = value),
{
limit: 30,
window: 2000,
windowType: this.windowType,
onReject: (rateLimiter) =>
console.log(
'Rejected by rate limiter',
rateLimiter.getMsUntilNextWindow(),
),
},
)
get rateLimitedSetValue() {
if (this.rateLimitedSetValueWindow !== this.windowType) {
this.rateLimitedSetValueWindow = this.windowType
this.rateLimitedSetValueFunction = rateLimit(
(value: typeof this.rateLimitedValue) =>
(this.rateLimitedValue = value),
{
limit: 30,
window: 2000,
windowType: this.windowType,
onReject: (rateLimiter) =>
console.log(
'Rejected by rate limiter',
rateLimiter.getMsUntilNextWindow(),
),
},
)
}
return this.rateLimitedSetValueFunction
}
handleRangeChange = (e: Event) => {
const newValue = parseInt((e.target as HTMLInputElement).value, 10)
this.currentValue = newValue
this.rateLimitedSetValue(newValue)
}
override createRenderRoot() {
return this
}
override render() {
return html`<div>
<h1>TanStack Pacer rateLimit Example 3</h1>
<div
style=${styleMap({ display: 'grid', gap: '0.5rem', marginBottom: '1rem' })}
>
<label
><input
type="radio"
name="windowType3"
value="fixed"
.checked=${this.windowType === 'fixed'}
@input=${() => (this.windowType = 'fixed')}
/>Fixed Window</label
><label
><input
type="radio"
name="windowType3"
value="sliding"
.checked=${this.windowType === 'sliding'}
@input=${() => (this.windowType = 'sliding')}
/>Sliding Window</label
>
</div>
<div style=${styleMap({ marginBottom: '20px' })}>
<label
>Current Range:<input
type="range"
min="0"
max="100"
.value=${this.currentValue}
@input=${this.handleRangeChange}
style=${styleMap({ width: '100%' })}
/><span>${this.currentValue}</span></label
>
</div>
<div style=${styleMap({ marginBottom: '20px' })}>
<label
>Rate Limited Range (Readonly):<input
type="range"
min="0"
max="100"
.value=${this.rateLimitedValue}
disabled
style=${styleMap({ width: '100%' })}
/><span>${this.rateLimitedValue}</span></label
>
</div>
<div style=${styleMap({ color: '#666', fontSize: '0.9em' })}>
<p>Rate limited to 30 updates per 2000ms window</p>
</div>
</div>`
}
}
customElements.define('pacer-range', Range)
class Example extends LitElement {
private devtools?: TanStackDevtoolsCore
private target?: HTMLDivElement
override createRenderRoot() {
return this
}
override connectedCallback() {
super.connectedCallback()
if (!import.meta.env.DEV) return
this.target = document.createElement('div')
document.body.append(this.target)
this.devtools = new TanStackDevtoolsCore({
plugins: [pacerDevtoolsPlugin()],
})
this.devtools.mount(this.target)
}
override disconnectedCallback() {
this.devtools?.unmount()
this.target?.remove()
this.devtools = undefined
this.target = undefined
super.disconnectedCallback()
}
override render() {
return html`<div>
<pacer-counter></pacer-counter>
<hr />
<pacer-search></pacer-search>
<hr />
<pacer-range></pacer-range>
</div>`
}
}
customElements.define('pacer-example', Example)
document.getElementById('app')!.append(document.createElement('pacer-example'))