Disposables
The disposable pattern is fundamental to resource management in True Life. It ensures proper cleanup of event subscriptions, timers, and other resources, preventing memory leaks and orphaned handlers.
Why Disposables?
In FiveM, resources can be started and stopped at any time. Without proper cleanup:
- Event handlers continue firing after module shutdown
- Timers keep running, causing errors
- Memory leaks accumulate over time
- RPC handlers remain registered, causing conflicts
The disposable pattern solves this by providing a consistent way to track and clean up resources.
Core Concepts
IDisposable Interface
interface IDisposable {
dispose(): void;
}
Any object with a dispose() method is disposable. When called, it should release all resources.
DisposableStore
A container that collects multiple disposables and disposes them together:
Basic Usage
Creating a Store
import { DisposableStore, toDisposable } from "@core/disposable";
const disposables = new DisposableStore();
Adding Disposables
// Event subscriptions return IDisposable
disposables.add(
fivemEvents.playerJoining.on((playerId) => {
console.log(`Player ${playerId} joining`);
})
);
// Observable subscriptions
disposables.add(
myObservable.subscribe((value) => {
console.log("Value changed:", value);
})
);
Custom Cleanup with toDisposable
// Wrap any cleanup function
const timer = setInterval(() => tick(), 1000);
disposables.add(
toDisposable(() => {
clearInterval(timer);
})
);
// Multiple cleanup actions
disposables.add(
toDisposable(() => {
clearInterval(timer1);
clearTimeout(timer2);
socket.close();
})
);
Disposing Everything
// Clean up all resources
disposables.dispose();
// After disposal, adding new items disposes them immediately
disposables.add(newEvent.on(() => {})); // Immediately disposed
Nested Stores
Create child stores for feature-specific cleanup:
const rootStore = new DisposableStore();
// Child store for a specific feature
const featureStore = rootStore.createChild();
featureStore.add(featureEvent.on(() => {}));
featureStore.add(toDisposable(() => clearFeatureState()));
// Another feature
const anotherStore = rootStore.createChild();
anotherStore.add(anotherEvent.on(() => {}));
// Dispose everything at once
rootStore.dispose(); // Also disposes featureStore and anotherStore
Nesting Diagram
Service Pattern
The recommended pattern for services:
export class MyService {
private disposables = new DisposableStore();
@ServerOnly
initServerSide(): void {
// Event subscriptions
this.disposables.add(
fivemEvents.playerJoining.on(this.handlePlayerJoining)
);
this.disposables.add(
fivemEvents.playerDropped.on(this.handlePlayerDropped)
);
// Timers
const updateTimer = setInterval(() => this.update(), 1000);
this.disposables.add(
toDisposable(() => clearInterval(updateTimer))
);
// Observable subscriptions
this.disposables.add(
this.stateObservable.subscribe((state) => {
this.onStateChanged(state);
})
);
}
@ServerOnly
cleanupServerSide(): void {
this.disposables.dispose();
}
private handlePlayerJoining = (playerId: number) => {
// Handle event
};
private handlePlayerDropped = (playerId: number, reason: string) => {
// Handle event
};
}
TypeScript using Declaration
DisposableStore supports native TypeScript using declarations:
function processData() {
using store = new DisposableStore();
store.add(tempResource.acquire());
store.add(toDisposable(() => cleanup()));
// Do work...
// Automatically disposed when function exits
}
API Reference
DisposableStore
| Method | Returns | Description |
|---|---|---|
add(disposable) | T | Add a disposable, returns it for chaining |
createChild() | DisposableStore | Create a nested child store |
dispose() | void | Dispose all items and mark store as disposed |
clear() | void | Remove all items without disposing them |
disposed | boolean | Check if store has been disposed |
size | number | Number of items in the store |
toDisposable
function toDisposable(dispose: () => void): IDisposable & Disposable
Creates a disposable from a cleanup function.
Common Patterns
Conditional Feature
class DebugService {
private disposables = new DisposableStore();
private debugStore: DisposableStore | null = null;
enableDebug(): void {
if (this.debugStore) return;
this.debugStore = this.disposables.createChild();
this.debugStore.add(debugEvent.on(() => {}));
this.debugStore.add(toDisposable(() => hideDebugUI()));
}
disableDebug(): void {
if (!this.debugStore) return;
this.debugStore.dispose();
this.debugStore = null;
}
}
Per-Player Resources
class PlayerService {
private disposables = new DisposableStore();
private playerStores = new Map<number, DisposableStore>();
onPlayerJoin(playerId: number): void {
const playerStore = this.disposables.createChild();
this.playerStores.set(playerId, playerStore);
// Player-specific resources
playerStore.add(this.watchPlayerPosition(playerId));
playerStore.add(this.trackPlayerStats(playerId));
}
onPlayerLeave(playerId: number): void {
const playerStore = this.playerStores.get(playerId);
if (playerStore) {
playerStore.dispose();
this.playerStores.delete(playerId);
}
}
cleanup(): void {
// Disposes root and all player stores
this.disposables.dispose();
this.playerStores.clear();
}
}
Chaining Returns
// add() returns the disposable for chaining
const subscription = disposables.add(
myEvent.on((data) => {
console.log(data);
})
);
// Can still access the subscription if needed
console.log(subscription.active);
Lifecycle Flow
Best Practices
- One store per service - Create a main
DisposableStorein your service - Add everything - Every subscription, timer, or resource should be tracked
- Use child stores - For features that can be enabled/disabled independently
- Dispose in cleanup - Always call
dispose()in your cleanup method - Arrow functions for handlers - Preserve
thiscontext in callbacks - Check disposed state - When adding conditionally, check
store.disposed
What Returns IDisposable?
Many True Life APIs return IDisposable:
| API | Returns |
|---|---|
event.on(callback) | Subscription disposable |
observable.subscribe(callback) | Subscription disposable |
observable.subscribePlayer(id) | Player subscription disposable |
observable.pipe(source) | Pipe disposable |
addPlayerSubscription(id, playerId) | Registry disposable |
toDisposable(fn) | Custom disposable |
DisposableStore.createChild() | Child store (also disposable) |
Debugging
Check Store Contents
const store = new DisposableStore();
store.add(event1.on(() => {}));
store.add(event2.on(() => {}));
console.log(`Store has ${store.size} items`);
console.log(`Store disposed: ${store.disposed}`);
Leak Detection
If you suspect leaks, add logging:
const tracked = toDisposable(() => {
console.log("Resource disposed!");
actualCleanup();
});
disposables.add(tracked);