@tmlmobilidade/controllers 20260203.1103.48 → 20260203.1129.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/rides/rides.js +10 -20
- package/dist/rides/watch.d.ts +56 -0
- package/dist/rides/watch.js +113 -0
- package/package.json +1 -1
package/dist/rides/rides.js
CHANGED
|
@@ -3,6 +3,7 @@ import { HttpStatus } from '@tmlmobilidade/consts';
|
|
|
3
3
|
import { rides, ridesBatchAggregationPipeline } from '@tmlmobilidade/interfaces';
|
|
4
4
|
import { normalizeRide } from '@tmlmobilidade/normalizers';
|
|
5
5
|
import { GetRidesBatchQuerySchema, PermissionCatalog } from '@tmlmobilidade/types';
|
|
6
|
+
import { ridesChangeStream } from './watch.js';
|
|
6
7
|
/* * */
|
|
7
8
|
export class RidesSharedController {
|
|
8
9
|
//
|
|
@@ -108,30 +109,19 @@ export class RidesSharedController {
|
|
|
108
109
|
socket.on('message', async () => {
|
|
109
110
|
//
|
|
110
111
|
//
|
|
111
|
-
//
|
|
112
|
-
const
|
|
113
|
-
//
|
|
114
|
-
// Start a watch service for the database
|
|
115
|
-
// and send updates to the client as they occur.
|
|
116
|
-
const changeStream = ridesCollection
|
|
117
|
-
.watch([], { fullDocument: 'updateLookup' })
|
|
118
|
-
.on('change', (databaseOperation) => {
|
|
119
|
-
if (typeof databaseOperation['fullDocument'] === 'undefined') {
|
|
120
|
-
console.log('Undefined document:', databaseOperation);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const normalizedRide = normalizeRide(databaseOperation['fullDocument']);
|
|
124
|
-
const message = {
|
|
125
|
-
data: normalizedRide,
|
|
126
|
-
error: null,
|
|
127
|
-
statusCode: HttpStatus.OK,
|
|
128
|
-
};
|
|
112
|
+
// Create a listener that sends updates to this WebSocket client
|
|
113
|
+
const listener = (message) => {
|
|
129
114
|
if (socket.readyState === socket.OPEN && socket.bufferedAmount < 1_000_000) {
|
|
130
115
|
socket.send(JSON.stringify(message));
|
|
131
116
|
}
|
|
132
|
-
}
|
|
117
|
+
};
|
|
118
|
+
//
|
|
119
|
+
// Subscribe to the singleton change stream
|
|
120
|
+
ridesChangeStream.subscribe(listener);
|
|
121
|
+
//
|
|
122
|
+
// Cleanup: unsubscribe when socket closes or errors
|
|
133
123
|
const cleanup = async () => {
|
|
134
|
-
|
|
124
|
+
ridesChangeStream.unsubscribe(listener);
|
|
135
125
|
};
|
|
136
126
|
socket.on('close', cleanup);
|
|
137
127
|
socket.on('error', cleanup);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { RideNormalized } from '@tmlmobilidade/types';
|
|
2
|
+
import { HttpResponse } from '@tmlmobilidade/utils';
|
|
3
|
+
/**
|
|
4
|
+
* A listener function for ride changes.
|
|
5
|
+
* Receives normalized ride data wrapped in an HTTP response format.
|
|
6
|
+
*/
|
|
7
|
+
export type RideChangeListener = (message: HttpResponse<RideNormalized>) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Singleton manager for MongoDB change streams with pub/sub capabilities.
|
|
10
|
+
*
|
|
11
|
+
* This class creates a single MongoDB change stream that watches for ride updates
|
|
12
|
+
* and broadcasts them to multiple subscribers using an in-memory EventEmitter.
|
|
13
|
+
*
|
|
14
|
+
* The singleton pattern ensures only one change stream is active, regardless of
|
|
15
|
+
* how many WebSocket clients are connected.
|
|
16
|
+
*/
|
|
17
|
+
declare class RidesChangeStreamManager {
|
|
18
|
+
private static instance;
|
|
19
|
+
/**
|
|
20
|
+
* In-memory pub/sub event emitter. (@see https://nodejs.org/api/events.html#class-eventemitter)
|
|
21
|
+
*
|
|
22
|
+
* This EventEmitter acts as the pub/sub broker between the MongoDB change stream
|
|
23
|
+
* and WebSocket clients:
|
|
24
|
+
* - The MongoDB change stream publishes to this emitter when rides change
|
|
25
|
+
* - WebSocket clients subscribe to this emitter to receive updates
|
|
26
|
+
* - When unsubscribing, clients are removed from the emitter's listener list
|
|
27
|
+
*
|
|
28
|
+
* Setting maxListeners to 0 allows unlimited subscribers without warnings,
|
|
29
|
+
* which is necessary since we may have many concurrent WebSocket connections.
|
|
30
|
+
*/
|
|
31
|
+
private emitter;
|
|
32
|
+
private initialized;
|
|
33
|
+
/**
|
|
34
|
+
* Private constructor enforces singleton pattern.
|
|
35
|
+
* Use `getInstance()` to access the instance.
|
|
36
|
+
*/
|
|
37
|
+
private constructor();
|
|
38
|
+
static getInstance(): Promise<RidesChangeStreamManager>;
|
|
39
|
+
subscribe(listener: RideChangeListener): void;
|
|
40
|
+
unsubscribe(listener: RideChangeListener): void;
|
|
41
|
+
/**
|
|
42
|
+
* Initializes the MongoDB change stream.
|
|
43
|
+
*
|
|
44
|
+
* This is called once during getInstance() to set up the change stream.
|
|
45
|
+
* The stream watches all operations on the rides collection and publishes
|
|
46
|
+
* changes to the EventEmitter.
|
|
47
|
+
*
|
|
48
|
+
* Flow:
|
|
49
|
+
* 1. MongoDB detects a change → 2. Change stream emits 'change' event →
|
|
50
|
+
* 3. Normalizes the ride data → 4. Publishes to EventEmitter →
|
|
51
|
+
* 5. All subscribed listeners receive the update
|
|
52
|
+
*/
|
|
53
|
+
private init;
|
|
54
|
+
}
|
|
55
|
+
export declare const ridesChangeStream: RidesChangeStreamManager;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
/**
|
|
3
|
+
* Rides Change Stream Manager
|
|
4
|
+
*
|
|
5
|
+
* This module implements a singleton pattern for MongoDB change streams with in-memory pub/sub:
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* ```
|
|
9
|
+
* MongoDB Change Stream (1 singleton)
|
|
10
|
+
* ↓
|
|
11
|
+
* EventEmitter pub/sub
|
|
12
|
+
* ↓
|
|
13
|
+
* WebSocket clients
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Instead of creating a MongoDB change stream per WebSocket connection (inefficient),
|
|
17
|
+
* this creates a single change stream that publishes to an in-memory EventEmitter.
|
|
18
|
+
* Multiple WebSocket clients can subscribe/unsubscribe to receive real-time updates.
|
|
19
|
+
*
|
|
20
|
+
* Benefits:
|
|
21
|
+
* - Single MongoDB change stream regardless of number of clients
|
|
22
|
+
* - Lazy initialization - stream only starts when first client connects through "AsyncSingletonProxy"
|
|
23
|
+
* - Clean subscription management per client
|
|
24
|
+
* - Reduced database load and network overhead
|
|
25
|
+
*/
|
|
26
|
+
import { HttpStatus } from '@tmlmobilidade/consts';
|
|
27
|
+
import { rides } from '@tmlmobilidade/interfaces';
|
|
28
|
+
import { normalizeRide } from '@tmlmobilidade/normalizers';
|
|
29
|
+
import { AsyncSingletonProxy } from '@tmlmobilidade/utils';
|
|
30
|
+
import EventEmitter from 'events';
|
|
31
|
+
/**
|
|
32
|
+
* Singleton manager for MongoDB change streams with pub/sub capabilities.
|
|
33
|
+
*
|
|
34
|
+
* This class creates a single MongoDB change stream that watches for ride updates
|
|
35
|
+
* and broadcasts them to multiple subscribers using an in-memory EventEmitter.
|
|
36
|
+
*
|
|
37
|
+
* The singleton pattern ensures only one change stream is active, regardless of
|
|
38
|
+
* how many WebSocket clients are connected.
|
|
39
|
+
*/
|
|
40
|
+
class RidesChangeStreamManager {
|
|
41
|
+
//
|
|
42
|
+
static instance = null;
|
|
43
|
+
/**
|
|
44
|
+
* In-memory pub/sub event emitter. (@see https://nodejs.org/api/events.html#class-eventemitter)
|
|
45
|
+
*
|
|
46
|
+
* This EventEmitter acts as the pub/sub broker between the MongoDB change stream
|
|
47
|
+
* and WebSocket clients:
|
|
48
|
+
* - The MongoDB change stream publishes to this emitter when rides change
|
|
49
|
+
* - WebSocket clients subscribe to this emitter to receive updates
|
|
50
|
+
* - When unsubscribing, clients are removed from the emitter's listener list
|
|
51
|
+
*
|
|
52
|
+
* Setting maxListeners to 0 allows unlimited subscribers without warnings,
|
|
53
|
+
* which is necessary since we may have many concurrent WebSocket connections.
|
|
54
|
+
*/
|
|
55
|
+
emitter = new EventEmitter();
|
|
56
|
+
initialized = false;
|
|
57
|
+
/**
|
|
58
|
+
* Private constructor enforces singleton pattern.
|
|
59
|
+
* Use `getInstance()` to access the instance.
|
|
60
|
+
*/
|
|
61
|
+
constructor() {
|
|
62
|
+
this.emitter.setMaxListeners(0); // Allow unlimited listeners
|
|
63
|
+
}
|
|
64
|
+
static async getInstance() {
|
|
65
|
+
if (!RidesChangeStreamManager.instance) {
|
|
66
|
+
RidesChangeStreamManager.instance = new RidesChangeStreamManager();
|
|
67
|
+
await RidesChangeStreamManager.instance.init();
|
|
68
|
+
}
|
|
69
|
+
return RidesChangeStreamManager.instance;
|
|
70
|
+
}
|
|
71
|
+
subscribe(listener) {
|
|
72
|
+
this.emitter.on('change', listener);
|
|
73
|
+
}
|
|
74
|
+
unsubscribe(listener) {
|
|
75
|
+
this.emitter.off('change', listener);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Initializes the MongoDB change stream.
|
|
79
|
+
*
|
|
80
|
+
* This is called once during getInstance() to set up the change stream.
|
|
81
|
+
* The stream watches all operations on the rides collection and publishes
|
|
82
|
+
* changes to the EventEmitter.
|
|
83
|
+
*
|
|
84
|
+
* Flow:
|
|
85
|
+
* 1. MongoDB detects a change → 2. Change stream emits 'change' event →
|
|
86
|
+
* 3. Normalizes the ride data → 4. Publishes to EventEmitter →
|
|
87
|
+
* 5. All subscribed listeners receive the update
|
|
88
|
+
*/
|
|
89
|
+
async init() {
|
|
90
|
+
if (this.initialized)
|
|
91
|
+
return;
|
|
92
|
+
const ridesCollection = await rides.getCollection();
|
|
93
|
+
// Watch all operations with full document updates
|
|
94
|
+
ridesCollection
|
|
95
|
+
.watch([], { fullDocument: 'updateLookup' })
|
|
96
|
+
.on('change', (databaseOperation) => {
|
|
97
|
+
if (typeof databaseOperation['fullDocument'] === 'undefined') {
|
|
98
|
+
console.log('Undefined document:', databaseOperation);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const normalizedRide = normalizeRide(databaseOperation['fullDocument']);
|
|
102
|
+
const message = {
|
|
103
|
+
data: normalizedRide,
|
|
104
|
+
error: null,
|
|
105
|
+
statusCode: HttpStatus.OK,
|
|
106
|
+
};
|
|
107
|
+
// Publish to all subscribers via EventEmitter
|
|
108
|
+
this.emitter.emit('change', message);
|
|
109
|
+
});
|
|
110
|
+
this.initialized = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export const ridesChangeStream = AsyncSingletonProxy(RidesChangeStreamManager);
|