Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pacer-gate-devtools-production.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/pacer': patch
---

Only run the devtools integration when `process.env.NODE_ENV === 'development'`, matching `@tanstack/devtools-event-client`. Keyed utils no longer register, clone state or subscribe to devtools events in production, and production bundles can now tree-shake the pacer event client and `@tanstack/devtools-event-client`.
6 changes: 5 additions & 1 deletion docs/reference/functions/emitChange.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ title: emitChange
function emitChange<TEvent>(event, instance): void;
```

Defined in: [event-client.ts:117](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/event-client.ts#L117)
Defined in: [event-client.ts:104](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/event-client.ts#L104)

Devtools only run in development, matching `@tanstack/devtools-event-client`.
Production bundlers fold this check, so the devtools registry, payload
cloning and the event client are tree-shaken out.

## Type Parameters

Expand Down
6 changes: 4 additions & 2 deletions docs/reference/variables/pacerEventClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ title: pacerEventClient
---

```ts
const pacerEventClient: PacerEventClient;
const pacerEventClient: EventClient<PacerEventMap>;
```

Defined in: [event-client.ts:129](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/event-client.ts#L129)
Defined in: [event-client.ts:122](https://github.com/TanStack/pacer/blob/main/packages/pacer/src/event-client.ts#L122)

A plain `EventClient` instance (no subclass) so bundlers can drop it when unused.
2 changes: 1 addition & 1 deletion packages/alpine-pacer/tsconfig.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
},
"rootDir": ".."
},
"include": ["src"]
"include": ["src", "../pacer/src/process-env.d.ts"]
}
2 changes: 1 addition & 1 deletion packages/angular-pacer/tsconfig.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"@tanstack/pacer/*": ["../pacer/src/*"]
}
},
"include": ["src"]
"include": ["src", "../pacer/src/process-env.d.ts"]
}
2 changes: 1 addition & 1 deletion packages/ember-pacer/tsconfig.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"rootDir": ".."
},
"include": ["src"],
"include": ["src", "../pacer/src/process-env.d.ts"],
"typedocOptions": {
"excludeExternals": true
}
Expand Down
2 changes: 1 addition & 1 deletion packages/lit-pacer/tsconfig.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
},
"rootDir": ".."
},
"include": ["src"]
"include": ["src", "../pacer/src/process-env.d.ts"]
}
2 changes: 1 addition & 1 deletion packages/octane-pacer/tsconfig.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
},
"rootDir": ".."
},
"include": ["src"]
"include": ["src", "../pacer/src/process-env.d.ts"]
}
2 changes: 1 addition & 1 deletion packages/pacer/src/async-batcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export class AsyncBatcher<TValue> {
}
this.#setState(this.options.initialState ?? {})

if (this.key) {
if (this.key && process.env.NODE_ENV === 'development') {
pacerEventClient.on('d-AsyncBatcher', (event) => {
if (event.payload.key !== this.key) return
this.#setState(
Expand Down
2 changes: 1 addition & 1 deletion packages/pacer/src/async-debouncer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
}
this.#setState(this.options.initialState ?? {})

if (this.key) {
if (this.key && process.env.NODE_ENV === 'development') {
pacerEventClient.on('d-AsyncDebouncer', (event) => {
if (event.payload.key !== this.key) return
this.#setState(
Expand Down
2 changes: 1 addition & 1 deletion packages/pacer/src/async-queuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ export class AsyncQueuer<TValue> {
}
}

if (this.key) {
if (this.key && process.env.NODE_ENV === 'development') {
pacerEventClient.on('d-AsyncQueuer', (e) => {
if (e.payload.key !== this.key) return
this.#setState(
Expand Down
2 changes: 1 addition & 1 deletion packages/pacer/src/async-rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
this.#setCleanupTimeout(executionTime)
}

if (this.key) {
if (this.key && process.env.NODE_ENV === 'development') {
pacerEventClient.on('d-AsyncRateLimiter', (event) => {
if (event.payload.key !== this.key) return
this.#setState(
Expand Down
2 changes: 1 addition & 1 deletion packages/pacer/src/async-throttler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
}
this.#setState(this.options.initialState ?? {})

if (this.key) {
if (this.key && process.env.NODE_ENV === 'development') {
pacerEventClient.on('d-AsyncThrottler', (event) => {
if (event.payload.key !== this.key) return
this.#setState(
Expand Down
2 changes: 1 addition & 1 deletion packages/pacer/src/batcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class Batcher<TValue> {
}
this.#setState(this.options.initialState ?? {})

if (this.key) {
if (this.key && process.env.NODE_ENV === 'development') {
pacerEventClient.on('d-Batcher', (event) => {
if (event.payload.key !== this.key) return
this.#setState(
Expand Down
2 changes: 1 addition & 1 deletion packages/pacer/src/debouncer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export class Debouncer<TFn extends AnyFunction> {
}
this.#setState(this.options.initialState ?? {})

if (this.key) {
if (this.key && process.env.NODE_ENV === 'development') {
pacerEventClient.on('d-Debouncer', (event) => {
if (event.payload.key !== this.key) return
this.#setState(
Expand Down
51 changes: 28 additions & 23 deletions packages/pacer/src/event-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,34 +96,39 @@ type PacerUtilEmitSource = {
options: unknown
}

class PacerEventClient extends EventClient<PacerEventMap> {
constructor(props?: { debug: boolean }) {
super({
pluginId: 'pacer',
debug: props?.debug,
/**
* TanStack Devtools starts `ClientEventBus` inside an async dynamic import
* after the first commit. Pacer utils often emit during the first render
* (constructor → store sync). `EventClient` only retries `tanstack-connect`
* `#maxRetries` times at `reconnectEveryMs` before it gives up and drops
* all queued events permanently. Default 300ms × 5 ≈ 1.5s is often too
* short; Form uses 1000ms here for the same reason.
*/
reconnectEveryMs: 1000,
})
}
}

/**
* Devtools only run in development, matching `@tanstack/devtools-event-client`.
* Production bundlers fold this check, so the devtools registry, payload
* cloning and the event client are tree-shaken out.
*/
export const emitChange = <TEvent extends keyof PacerEventMap>(
event: TEvent,
instance: PacerUtilEmitSource,
) => {
const key = instance.key
if (!key) {
return
// Check `key` first: unbundled Node evaluates `process.env` on every call.
if (key && process.env.NODE_ENV === 'development') {
registerPacerDevtoolsInstance(key, instance)
pacerEventClient.emit(
event,
toPacerDevtoolsWirePayload({ ...instance, key }),
)
}
registerPacerDevtoolsInstance(key, instance)
pacerEventClient.emit(event, toPacerDevtoolsWirePayload({ ...instance, key }))
}

export const pacerEventClient = new PacerEventClient()
/**
* A plain `EventClient` instance (no subclass) so bundlers can drop it when unused.
*/
export const pacerEventClient: EventClient<PacerEventMap> =
/* @__PURE__ */ new EventClient<PacerEventMap>({
pluginId: 'pacer',
/**
* TanStack Devtools starts `ClientEventBus` inside an async dynamic import
* after the first commit. Pacer utils often emit during the first render
* (constructor → store sync). `EventClient` only retries `tanstack-connect`
* `#maxRetries` times at `reconnectEveryMs` before it gives up and drops
* all queued events permanently. Default 300ms × 5 ≈ 1.5s is often too
* short; Form uses 1000ms here for the same reason.
*/
reconnectEveryMs: 1000,
})
6 changes: 6 additions & 0 deletions packages/pacer/src/process-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** Minimal typing for `process.env.NODE_ENV` checks without pulling in full Node types. */
declare const process: {
env: {
NODE_ENV?: string
}
}
2 changes: 1 addition & 1 deletion packages/pacer/src/queuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ export class Queuer<TValue> {
}
}

if (this.key) {
if (this.key && process.env.NODE_ENV === 'development') {
pacerEventClient.on('d-Queuer', (event) => {
if (event.payload.key !== this.key) return
this.#setState(
Expand Down
2 changes: 1 addition & 1 deletion packages/pacer/src/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export class RateLimiter<TFn extends AnyFunction> {
this.#setCleanupTimeout(executionTime)
}

if (this.key) {
if (this.key && process.env.NODE_ENV === 'development') {
pacerEventClient.on('d-RateLimiter', (event) => {
if (event.payload.key !== this.key) return
this.#setState(event.payload.store.state as Partial<RateLimiterState>)
Expand Down
2 changes: 1 addition & 1 deletion packages/pacer/src/throttler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export class Throttler<TFn extends AnyFunction> {
}
this.#setState(this.options.initialState ?? {})

if (this.key) {
if (this.key && process.env.NODE_ENV === 'development') {
pacerEventClient.on('d-Throttler', (event) => {
if (event.payload.key !== this.key) return
this.#setState(
Expand Down
2 changes: 2 additions & 0 deletions packages/pacer/tests/async-debouncer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1373,9 +1373,11 @@ describe('asyncDebounce helper function', () => {
describe('AsyncDebouncer internal retryer devtools registration', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.stubEnv('NODE_ENV', 'development')
})
afterEach(() => {
vi.useRealTimers()
vi.unstubAllEnvs()
})

it('should not register internal retryers when the debouncer has no key', async () => {
Expand Down
7 changes: 7 additions & 0 deletions packages/pacer/tests/async-queuer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,13 @@ describe('AsyncQueuer', () => {
})

describe('internal retryer devtools registration', () => {
beforeEach(() => {
vi.stubEnv('NODE_ENV', 'development')
})
afterEach(() => {
vi.unstubAllEnvs()
})

it('should not register internal retryers when the queuer has no key', async () => {
const asyncQueuer = new AsyncQueuer<number>(
(item) => Promise.resolve(item * 2),
Expand Down
2 changes: 2 additions & 0 deletions packages/pacer/tests/async-rate-limiter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,9 +789,11 @@ describe('asyncRateLimit', () => {
describe('AsyncRateLimiter internal retryer devtools registration', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.stubEnv('NODE_ENV', 'development')
})
afterEach(() => {
vi.useRealTimers()
vi.unstubAllEnvs()
})

it('should not register internal retryers with or without a key', async () => {
Expand Down
9 changes: 8 additions & 1 deletion packages/pacer/tests/async-retryer.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AsyncRetryer, asyncRetry } from '../src/async-retryer'
import { getPacerDevtoolsInstance } from '../src'

Expand Down Expand Up @@ -1146,6 +1146,13 @@ describe('asyncRetry utility function', () => {
})

describe('AsyncRetryer devtools registration', () => {
beforeEach(() => {
vi.stubEnv('NODE_ENV', 'development')
})
afterEach(() => {
vi.unstubAllEnvs()
})

it('should not register with the devtools registry even when keyed', async () => {
const retryer = new AsyncRetryer(
(value: string) => Promise.resolve(value),
Expand Down
2 changes: 2 additions & 0 deletions packages/pacer/tests/async-throttler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1012,9 +1012,11 @@ describe('AsyncThrottler', () => {
describe('AsyncThrottler internal retryer devtools registration', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.stubEnv('NODE_ENV', 'development')
})
afterEach(() => {
vi.useRealTimers()
vi.unstubAllEnvs()
})

it('should not register internal retryers with or without a key', async () => {
Expand Down
43 changes: 43 additions & 0 deletions packages/pacer/tests/event-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Throttler } from '../src/throttler'
import { getPacerDevtoolsInstance, pacerEventClient } from '../src/event-client'

describe('pacer devtools event client', () => {
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllEnvs()
})

it('should register, emit a cloned payload and subscribe in development', () => {
vi.stubEnv('NODE_ENV', 'development')
const emit = vi.spyOn(pacerEventClient, 'emit')
const on = vi.spyOn(pacerEventClient, 'on')

const throttler = new Throttler(() => {}, { wait: 100, key: 'dev-key' })

expect(getPacerDevtoolsInstance('dev-key')).toBe(throttler)
expect(on).toHaveBeenCalledWith('d-Throttler', expect.any(Function))
expect(emit).toHaveBeenCalledWith('Throttler', {
key: 'dev-key',
store: { state: throttler.store.state },
options: expect.objectContaining({ wait: 100, key: 'dev-key' }),
})
const payload = emit.mock.lastCall![1]
expect(payload.store.state).not.toBe(throttler.store.state)
})

it('should not register, clone, emit or subscribe outside development', () => {
vi.stubEnv('NODE_ENV', 'production')
const emit = vi.spyOn(pacerEventClient, 'emit')
const on = vi.spyOn(pacerEventClient, 'on')
const stringify = vi.spyOn(JSON, 'stringify')

const throttler = new Throttler(() => {}, { wait: 100, key: 'prod-key' })
throttler.maybeExecute()

expect(getPacerDevtoolsInstance('prod-key')).toBeUndefined()
expect(emit).not.toHaveBeenCalled()
expect(on).not.toHaveBeenCalled()
expect(stringify).not.toHaveBeenCalled()
})
})
2 changes: 1 addition & 1 deletion packages/preact-pacer/tsconfig.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"@tanstack/pacer": ["../pacer/src"]
}
},
"include": ["src"]
"include": ["src", "../pacer/src/process-env.d.ts"]
}
2 changes: 1 addition & 1 deletion packages/react-pacer/tsconfig.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"@tanstack/pacer": ["../pacer/src"]
}
},
"include": ["src"]
"include": ["src", "../pacer/src/process-env.d.ts"]
}
2 changes: 1 addition & 1 deletion packages/solid-pacer/tsconfig.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"@tanstack/pacer": ["../pacer/src"]
}
},
"include": ["src"]
"include": ["src", "../pacer/src/process-env.d.ts"]
}
2 changes: 1 addition & 1 deletion packages/svelte-pacer/tsconfig.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
},
"rootDir": ".."
},
"include": ["src"]
"include": ["src", "../pacer/src/process-env.d.ts"]
}
2 changes: 1 addition & 1 deletion packages/vue-pacer/tsconfig.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
},
"rootDir": ".."
},
"include": ["src"]
"include": ["src", "../pacer/src/process-env.d.ts"]
}
Loading