jrx
v0.1.0
Published
A lightweight TypeScript library for managing side effects, subscriptions, and animations with automatic cleanup. Built on top of [jdisposer](https://github.com/tranvansang/jdisposer) for safe resource management.
Downloads
365
Readme
jrx
A lightweight TypeScript library for managing side effects, subscriptions, and animations with automatic cleanup. Built on top of jdisposer for safe resource management.
Installation
npm install jrxFeatures
- Automatic cleanup for all effects and subscriptions
- Type-safe disposer pattern
- Retry logic with exponential backoff and cancellation
- Single dependency (jdisposer)
- Composable reactive utilities
- Browser and Node.js compatible
API Overview
makeRenderLoop()- Render loops with automatic cleanupaddInterval(cb, ms)- Repeating intervals with cleanupaddIntervalAsync(cb, ms)- Async intervals with cancellationaddRequestAnimationFrame(cb)- Single animation frame with cleanupaddRequestAnimationFrameLoop(cb)- Animation frame loopsaddSubs(subs, cb, options?)- Multiple subscription managementaddTimeout(cb, ms)- Timeouts with cleanupaddTransition(cb, durationMs)- Progress-based animationscomputed(fn, getDeps?)- Memoized computed valuesretry(cb, options?)- Async retry with exponential backoff
API
makeRenderLoop()
Creates a render loop with automatic cleanup management.
import { makeRenderLoop } from 'jrx'
const { loop, setLoop } = makeRenderLoop()
// Set the loop function
const dispose = setLoop((time) => {
console.log('Frame time:', time)
// Optional: return cleanup function
return () => {
console.log('Cleanup previous frame')
}
})
// Call loop on each animation frame
requestAnimationFrame(loop)
// Cleanup
dispose()addInterval(cb, ms)
Creates a repeating interval with cleanup. The callback can optionally return a cleanup function that runs before the next invocation.
Note: The callback fires immediately on first call, then waits ms milliseconds after the previous callback completes. This is not a fixed-rate timer.
import { addInterval } from 'jrx'
const dispose = addInterval(() => {
console.log('Tick') // Called immediately, then every 1000ms after completion
// Optional: return cleanup function
return () => {
console.log('Cleanup')
}
}, 1000)
// Stop the interval
dispose()addIntervalAsync(cb, ms)
Async version of addInterval. Waits for the callback to complete before scheduling the next invocation.
Note: The callback fires immediately on first call, then waits ms milliseconds after the previous async callback completes.
import { addIntervalAsync } from 'jrx'
const dispose = addIntervalAsync(async (disposer) => {
// Called immediately, then 5000ms after each completion
await fetchData()
// Check if disposed during async operation
if (disposer.signal.aborted) return
processData()
}, 5000)
dispose()addRequestAnimationFrame(cb)
Executes a callback on the next animation frame with cleanup.
import { addRequestAnimationFrame } from 'jrx'
const dispose = addRequestAnimationFrame((now) => {
updateAnimation(now)
// Optional: return cleanup function
return () => {
cleanupAnimation()
}
})
// Cancel if needed before the frame fires
dispose()addRequestAnimationFrameLoop(cb)
Creates a continuous requestAnimationFrame loop with cleanup.
import { addRequestAnimationFrameLoop } from 'jrx'
const dispose = addRequestAnimationFrameLoop((now) => {
updateAnimation(now)
// Optional: return cleanup function
return () => {
cleanupAnimation()
}
})
// Stop the loop
dispose()addSubs(subs, cb, options?)
Manages multiple subscriptions with a single callback.
import { addSubs } from 'jrx'
const sub1 = (listener) => {
eventEmitter.on('event1', listener)
return () => eventEmitter.off('event1', listener)
}
const sub2 = (listener) => {
eventEmitter.on('event2', listener)
return () => eventEmitter.off('event2', listener)
}
const dispose = addSubs([sub1, sub2], () => {
console.log('Any event fired')
// Optional: return cleanup function
return () => {
console.log('Cleanup')
}
}, { now: true }) // Call immediately with now: true
dispose()addTimeout(cb, ms)
Creates a timeout with cleanup.
import { addTimeout } from 'jrx'
const cancel = addTimeout(() => {
console.log('Timeout fired')
}, 1000)
// Cancel if needed
cancel()addTransition(cb, durationMs)
Creates an animation transition with progress tracking (0 to 1).
import { addTransition } from 'jrx'
const dispose = addTransition((progress) => {
element.style.opacity = progress.toString()
// Optional: return cleanup function
return () => {
console.log('Frame cleanup')
}
}, 1000)
dispose()computed(fn, getDeps?)
Creates a memoized computed value with optional dependency tracking.
import { computed } from 'jrx'
// Without dependencies - always recomputes
const value1 = computed(() => expensiveCalculation())
console.log(value1.value) // Computed
console.log(value1.value) // Computed again
// With dependencies - memoizes when deps unchanged
let a = 1, b = 2
const value2 = computed(
() => a + b,
() => [a, b] // Dependencies
)
console.log(value2.value) // Computed: 3
console.log(value2.value) // Cached: 3
a = 10
console.log(value2.value) // Recomputed: 12retry(cb, options?)
Retries an async operation with exponential backoff on failure.
Default backoff: [5, 5, 10, 10, 20, 20, 40, 40, 60, -1] seconds (where -1 means retry forever with 60s delay)
import retry from 'jrx/retry'
// Basic usage - retries with default backoff
const result = await retry(async (disposer, { resetBackoff }) => {
const response = await fetch('/api/data')
if (!response.ok) throw new Error('Failed to fetch')
return response.json()
})
// Custom backoff schedule (in seconds)
await retry(
async (disposer, { resetBackoff }) => {
return await fetchData()
},
{
backoffSec: [1, 2, 5, 10, -1] // -1 means retry forever with last delay
}
)
// With disposer for cancellation
import { makeDisposer } from 'jdisposer'
const disposer = makeDisposer()
const data = await retry(
async (loopDisposer, { resetBackoff }) => {
// Check if aborted
if (loopDisposer.signal.aborted) return
const result = await fetchData()
// Reset backoff on successful partial progress
if (result.isPartialSuccess) {
resetBackoff()
}
return result
},
{
disposer,
backoffSec: [5, 10, 20, 40, -1]
}
)
// Cancel the retry loop
disposer.dispose()
// Returns undefined when disposed
console.log(data) // T | undefinedOptions:
backoffSec: Array of retry delays in seconds. Use-1for infinite retries with the last delay.- Default:
[5, 5, 10, 10, 20, 20, 40, 40, 60, -1]
- Default:
disposer: Optional disposer for cancellation. When provided, the return type isT | undefined. Otherwise, the return type isT.
Callback parameters:
disposer: A disposer for the current retry attempt. Checkdisposer.signal.abortedto handle cancellationinfo.resetBackoff(): Call this to reset the backoff counter to the beginning (useful when making partial progress)
Cleanup Pattern
All functions return disposer functions that clean up resources:
import {addInterval, addTimeout, addRequestAnimationFrame} from 'jrx'
import {makeDisposer} from 'jdisposer'
const disposer = makeDisposer()
// Collect disposers
disposer.add(addInterval(() => console.log('tick'), 1000))
disposer.add(addTimeout(() => console.log('timeout'), 5000))
disposer.add(addRequestAnimationFrameLoop((now) => render(now)))
// Cleanup all at once
disposer.dispose()TypeScript
This library is written in TypeScript and includes type definitions.
import type { Disposer } from 'jdisposer'
// All disposer functions follow this pattern
type DisposerFunction = () => voidLicense
MIT
Repository
https://github.com/tranvansang/jrx
