@nestbolt/sortable
Events
Listen to position-changed and reordered events via @nestjs/event-emitter.
@nestbolt/sortable emits events when an entity's position changes or when a list is bulk-reordered. Events are dispatched via @nestjs/event-emitter when it's installed and EventEmitterModule is registered.
Setup
Install and register the event emitter module:
npm install @nestjs/event-emitterimport { EventEmitterModule } from "@nestjs/event-emitter";
import { SortableModule } from "@nestbolt/sortable";
@Module({
imports: [
EventEmitterModule.forRoot(),
SortableModule.forRoot(),
// ...
],
})
export class AppModule {}If @nestjs/event-emitter is not installed or EventEmitterModule is not registered, the package works normally but no events are emitted. The event emitter is resolved lazily and treated as fully optional.
Events
sortable.position-changed
Emitted after a successful moveTo(), moveUp(), moveDown(), moveToTop(), or moveToBottom() call when the position actually changed. If the move was a no-op (e.g., moveUp() on a row already at startPosition, or moveTo() to the row's current position), no event is emitted.
import { OnEvent } from "@nestjs/event-emitter";
import { SORTABLE_EVENTS, type PositionChangedEvent } from "@nestbolt/sortable";
@Injectable()
export class SortableListener {
@OnEvent(SORTABLE_EVENTS.POSITION_CHANGED)
handlePositionChanged(event: PositionChangedEvent) {
console.log(
`${event.entityType}#${event.entityId}: ${event.oldPosition} -> ${event.newPosition}`,
);
}
}Payload: PositionChangedEvent
| Field | Type | Description |
|---|---|---|
entityType | string | The entity class name (Task.name === "Task") |
entityId | SortableId (string | number) | The ID of the entity that moved |
oldPosition | number | The position before the move |
newPosition | number | The position after the move (already clamped) |
sortable.reordered
Emitted after a successful reorder() call.
import { OnEvent } from "@nestjs/event-emitter";
import { SORTABLE_EVENTS, type ReorderedEvent } from "@nestbolt/sortable";
@Injectable()
export class SortableListener {
@OnEvent(SORTABLE_EVENTS.REORDERED)
handleReordered(event: ReorderedEvent) {
console.log(
`${event.entityType} reordered (${event.items.length} items)`,
);
}
}Payload: ReorderedEvent
| Field | Type | Description |
|---|---|---|
entityType | string | The entity class name |
items | Array<{ id: SortableId; position: number }> | Final positions of every reordered row, in input order |
Event Constants
Use the SORTABLE_EVENTS constant for type-safe event names:
import { SORTABLE_EVENTS } from "@nestbolt/sortable";
SORTABLE_EVENTS.POSITION_CHANGED; // "sortable.position-changed"
SORTABLE_EVENTS.REORDERED; // "sortable.reordered"Practical Example: Sync to a Cache
A common use case is invalidating or refreshing a cached ordered list whenever the canonical order changes:
@Injectable()
export class TaskOrderCache {
constructor(private readonly cache: CacheService) {}
@OnEvent(SORTABLE_EVENTS.POSITION_CHANGED)
async onPositionChanged(event: PositionChangedEvent) {
if (event.entityType !== "Task") return;
await this.cache.del(`tasks:order`);
}
@OnEvent(SORTABLE_EVENTS.REORDERED)
async onReordered(event: ReorderedEvent) {
if (event.entityType !== "Task") return;
await this.cache.del(`tasks:order`);
}
}Filter on entityType so a single listener can handle one entity type at a time, even though all sortable events share the same event names.
Practical Example: Push to Connected Clients
If you have a websocket layer, broadcast position changes so other clients can update their views in real time:
@Injectable()
export class TaskOrderBroadcast {
constructor(private readonly socket: SocketService) {}
@OnEvent(SORTABLE_EVENTS.POSITION_CHANGED)
onPositionChanged(event: PositionChangedEvent) {
if (event.entityType !== "Task") return;
this.socket.broadcast("tasks:moved", {
id: event.entityId,
from: event.oldPosition,
to: event.newPosition,
});
}
@OnEvent(SORTABLE_EVENTS.REORDERED)
onReordered(event: ReorderedEvent) {
if (event.entityType !== "Task") return;
this.socket.broadcast("tasks:reordered", { items: event.items });
}
}Reliability
Events are emitted after the database write commits. If your handler throws, the move / reorder itself is unaffected -- the row state is already committed. Events are best-effort signals, not transactional outbox messages.
If the move was a no-op (e.g., moveTo() to the same position the row already had, or moveUp() at startPosition), no event is emitted -- this keeps cache invalidation listeners and websocket broadcasts from firing for changes that didn't actually change anything.
The auto-position-on-insert performed by SortableSubscriber does not emit sortable.position-changed. That event represents an explicit move, not the initial assignment when a new row is inserted.