Skip to main content

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

MethodReturnsDescription
add(disposable)TAdd a disposable, returns it for chaining
createChild()DisposableStoreCreate a nested child store
dispose()voidDispose all items and mark store as disposed
clear()voidRemove all items without disposing them
disposedbooleanCheck if store has been disposed
sizenumberNumber 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

  1. One store per service - Create a main DisposableStore in your service
  2. Add everything - Every subscription, timer, or resource should be tracked
  3. Use child stores - For features that can be enabled/disabled independently
  4. Dispose in cleanup - Always call dispose() in your cleanup method
  5. Arrow functions for handlers - Preserve this context in callbacks
  6. Check disposed state - When adding conditionally, check store.disposed

What Returns IDisposable?

Many True Life APIs return IDisposable:

APIReturns
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);