/Docs

CleanupRegistry

Every long-lived resource the SDK creates — polling timers, streaming connections, flag-change listeners, async iterators, decision recorder flush timers — is registered in a CleanupRegistry. When you call client.close(), the registry drains every registered teardown function in reverse-registration order. No competitor manages this automatically.

What it is

The CleanupRegistry is a thin ordered list of teardown callbacks. Every time the SDK internally creates a resource with a lifetime beyond a single request — a setInterval for polling, an EventSource for streaming, a middleware binding, an onFlagChange subscription — it calls registry.add(teardownFn) and receives an unregistration function in return. When the client closes, registry.drain() calls every registered teardown, awaits any that return promises, and clears the list.

ts
1// Simplified implementation
2class CleanupRegistry {
3 private readonly fns: Array<() => void | Promise<void>> = []
4
5 add(fn: () => void | Promise<void>): () => void {
6 this.fns.push(fn)
7 return () => {
8 const i = this.fns.indexOf(fn)
9 if (i !== -1) this.fns.splice(i, 1)
10 }
11 }
12
13 async drain(): Promise<void> {
14 const snapshot = this.fns.splice(0)
15 for (const fn of snapshot.reverse()) {
16 await fn()
17 }
18 }
19}

When to use it

You do not need to use CleanupRegistry directly in most applications — the SDK manages it internally. You interact with it in two scenarios:

  • Custom integrations: if you attach a custom listener or sink that has its own teardown (e.g., a WebSocket connection, an external flush timer), call client.utils.registerCleanup(fn) to ensure it is torn down when client.close() runs.
  • Middleware: if you use a framework middleware adapter from @avsbhq/utils/middleware, the middleware registers its own request-scoped resources into a per-request registry that is drained at the end of each request lifecycle.

Why this matters

Three environments make resource leaks acutely painful:

  • Node.js serverless (Lambda, Vercel Functions): a warm function instance is reused across invocations. If a polling interval from a previous SDK initialisation is never cleared, it accumulates across warm invocations and eventually causes stale-data bugs or CPU spikes.
  • React Strict Mode: development mode mounts and immediately unmounts every component twice to surface missing cleanup. An SDK client initialised in a useEffect without a proper return-teardown will open two polling connections. The React adapter calls client.close() in its useEffect cleanup, and CleanupRegistry ensures that call is idempotent — draining an already-drained registry is a no-op.
  • Test suites: Jest and Vitest keep the Node.js process alive between test files. A leaked setInterval from an SDK client opened in one test will fire during another test and produce unexpected state mutations.
Tip
Always call client.close()in your framework's cleanup hook. For React, that means returning it from useEffect. For Express/Fastify, call it during SIGTERM handling before the process exits.

How it works in practice

@avsbhq/browser — React cleanup
ts
1import { useEffect } from 'react'
2import { AvsbClient } from '@avsbhq/browser'
3
4// In your provider component
5useEffect(() => {
6 const client = new AvsbClient({ sdkKey: 'sdk_production_abc123' })
7 client.onReady().then(() => setClient(client))
8
9 // CleanupRegistry drains: polling timer + any listeners
10 return () => { client.close() }
11}, [])
@avsbhq/node — registering custom cleanup
ts
1import { AvsbServer } from '@avsbhq/node'
2
3const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY })
4await server.onReady()
5
6// Your own resource
7const flushInterval = setInterval(() => myBatchFlush(), 10_000)
8
9// Register teardown so client.close() clears it
10server.utils.registerCleanup(() => clearInterval(flushInterval))
11
12// On SIGTERM / graceful shutdown
13process.on('SIGTERM', async () => {
14 await server.close() // drains registry: polling, streaming, your flush timer
15 process.exit(0)
16})
@avsbhq/react — provider teardown
ts
1import { AvsbProvider } from '@avsbhq/react'
2
3// AvsbProvider calls client.close() in its own useEffect cleanup
4// You do not need to call it manually when using the provider
5function App() {
6 return (
7 <AvsbProvider sdkKey="sdk_production_abc123" attributes={{ userId: 'u_1' }}>
8 <YourApp />
9 </AvsbProvider>
10 )
11}
@avsbhq/utils — CleanupRegistry directly
ts
1import { CleanupRegistry } from '@avsbhq/utils'
2
3// Use the registry standalone to manage any set of teardowns
4const registry = new CleanupRegistry()
5
6const unregisterA = registry.add(() => clearInterval(timerA))
7const unregisterB = registry.add(async () => { await ws.close() })
8
9// Later: remove one resource before drain
10unregisterA()
11
12// Or drain all remaining
13await registry.drain()