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.
1// Simplified implementation2class 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 whenclient.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
useEffectwithout a proper return-teardown will open two polling connections. The React adapter callsclient.close()in itsuseEffectcleanup, andCleanupRegistryensures 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
setIntervalfrom an SDK client opened in one test will fire during another test and produce unexpected state mutations.
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
1import { useEffect } from 'react'2import { AvsbClient } from '@avsbhq/browser'3
4// In your provider component5useEffect(() => {6 const client = new AvsbClient({ sdkKey: 'sdk_production_abc123' })7 client.onReady().then(() => setClient(client))8
9 // CleanupRegistry drains: polling timer + any listeners10 return () => { client.close() }11}, [])1import { AvsbServer } from '@avsbhq/node'2
3const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY })4await server.onReady()5
6// Your own resource7const flushInterval = setInterval(() => myBatchFlush(), 10_000)8
9// Register teardown so client.close() clears it10server.utils.registerCleanup(() => clearInterval(flushInterval))11
12// On SIGTERM / graceful shutdown13process.on('SIGTERM', async () => {14 await server.close() // drains registry: polling, streaming, your flush timer15 process.exit(0)16})1import { AvsbProvider } from '@avsbhq/react'2
3// AvsbProvider calls client.close() in its own useEffect cleanup4// You do not need to call it manually when using the provider5function App() {6 return (7 <AvsbProvider sdkKey="sdk_production_abc123" attributes={{ userId: 'u_1' }}>8 <YourApp />9 </AvsbProvider>10 )11}1import { CleanupRegistry } from '@avsbhq/utils'2
3// Use the registry standalone to manage any set of teardowns4const 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 drain10unregisterA()11
12// Or drain all remaining13await registry.drain()