kuzzle 2.14.10 → 2.14.14

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.
Files changed (37) hide show
  1. package/.kuzzlerc.sample +6 -0
  2. package/lib/api/funnel.js +9 -5
  3. package/lib/cluster/node.js +5 -0
  4. package/lib/cluster/state.d.ts +2 -11
  5. package/lib/config/default.config.js +3 -1
  6. package/lib/config/index.js +6 -1
  7. package/lib/core/auth/tokenManager.d.ts +7 -0
  8. package/lib/core/auth/tokenManager.js +25 -8
  9. package/lib/core/network/router.js +0 -4
  10. package/lib/core/realtime/channel.d.ts +64 -0
  11. package/lib/core/realtime/channel.js +109 -0
  12. package/lib/core/realtime/connectionRooms.d.ts +28 -0
  13. package/lib/core/realtime/connectionRooms.js +68 -0
  14. package/lib/core/realtime/hotelClerk.d.ts +140 -0
  15. package/lib/core/realtime/hotelClerk.js +427 -630
  16. package/lib/core/realtime/index.js +1 -1
  17. package/lib/core/realtime/notifier.js +6 -6
  18. package/lib/core/realtime/room.d.ts +66 -0
  19. package/lib/core/realtime/room.js +103 -0
  20. package/lib/core/realtime/subscription.d.ts +25 -0
  21. package/lib/core/realtime/subscription.js +48 -0
  22. package/lib/core/statistics/statistics.js +43 -5
  23. package/lib/kerror/codes/1-services.json +11 -0
  24. package/lib/kerror/codes/2-api.json +1 -1
  25. package/lib/kuzzle/kuzzle.js +2 -2
  26. package/lib/types/KuzzleDocument.d.ts +5 -0
  27. package/lib/types/KuzzleDocument.js +3 -0
  28. package/lib/types/index.d.ts +4 -0
  29. package/lib/types/index.js +4 -0
  30. package/lib/types/realtime/RealtimeScope.d.ts +4 -0
  31. package/lib/types/realtime/RealtimeScope.js +3 -0
  32. package/lib/types/realtime/RealtimeUsers.d.ts +4 -0
  33. package/lib/types/realtime/RealtimeUsers.js +3 -0
  34. package/lib/types/realtime/RoomList.d.ts +20 -0
  35. package/lib/types/realtime/RoomList.js +3 -0
  36. package/package-lock.json +361 -152
  37. package/package.json +15 -15
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  /*
2
3
  * Kuzzle, a backend software, self-hostable and ready to use
3
4
  * to power modern apps
@@ -18,665 +19,461 @@
18
19
  * See the License for the specific language governing permissions and
19
20
  * limitations under the License.
20
21
  */
21
-
22
- 'use strict';
23
-
24
- const Bluebird = require('bluebird');
25
-
26
- const { Request, RequestContext } = require('../../api/request');
27
- const kerror = require('../../kerror');
28
- const debug = require('../../util/debug')('kuzzle:realtime:hotelClerk');
29
- const {
30
- getCollections,
31
- toKoncordeIndex,
32
- } = require('../../util/koncordeCompat');
33
-
34
- const realtimeError = kerror.wrap('core', 'realtime');
35
-
36
- const CHANNEL_ALLOWED_VALUES = ['all', 'in', 'out', 'none'];
37
-
38
- class Channel {
39
- constructor (roomId, { scope='all', users='none', propagate=true } = {}) {
40
- this.scope = scope;
41
- this.users = users;
42
- this.cluster = propagate;
43
-
44
- if (! CHANNEL_ALLOWED_VALUES.includes(this.scope)) {
45
- throw realtimeError.get('invalid_scope');
46
- }
47
-
48
- if (! CHANNEL_ALLOWED_VALUES.includes(this.users)) {
49
- throw realtimeError.get('invalid_users');
50
- }
51
-
52
- this.name = `${roomId}-${global.kuzzle.hash(this)}`;
53
- }
54
- }
55
-
56
- class Subscription {
57
- constructor (index, collection, filters, roomId, connectionId, user) {
58
- this.index = index;
59
- this.collection = collection;
60
- this.filters = filters;
61
- this.roomId = roomId;
62
- this.connectionId = connectionId;
63
- this.kuid = user && user._id || null;
64
- }
65
- }
66
-
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.HotelClerk = void 0;
27
+ const bluebird_1 = __importDefault(require("bluebird"));
28
+ const request_1 = require("../../api/request");
29
+ const kerror_1 = __importDefault(require("../../kerror"));
30
+ const debug_1 = __importDefault(require("../../util/debug"));
31
+ const koncordeCompat_1 = require("../../util/koncordeCompat");
32
+ const channel_1 = require("./channel");
33
+ const connectionRooms_1 = require("./connectionRooms");
34
+ const room_1 = require("./room");
35
+ const subscription_1 = require("./subscription");
36
+ const realtimeError = kerror_1.default.wrap('core', 'realtime');
37
+ const debug = (0, debug_1.default)('kuzzle:realtime:hotelClerk');
38
+ /**
39
+ * The HotelClerk is responsible of keeping the list of rooms and subscriptions
40
+ * made to those rooms.
41
+ *
42
+ * When a subscription is made to a room, the HotelClerk link the connection
43
+ * to a channel of this room. Each channel represents a specific configuration
44
+ * about which kind of notification the subscriber should receive (e.g. scope in/out)
45
+ *
46
+ * When an user is subscribing, we send him back the channel he is subscribing to.
47
+ *
48
+ * Here stop the role of the HotelClerk, then the notifier will select the channels
49
+ * according to the notification and notify them.
50
+ */
67
51
  class HotelClerk {
68
- constructor (realtimeModule) {
69
- this.module = realtimeModule;
70
-
52
+ constructor(realtimeModule) {
53
+ /**
54
+ * Number of created rooms.
55
+ *
56
+ * Used with the "subscriptionRooms" configuration limit.
57
+ */
58
+ this.roomsCount = 0;
59
+ /**
60
+ * Current realtime rooms.
61
+ *
62
+ * This object is used by the notifier to list wich channel has to be notified
63
+ * when a subscription scope is matching.
64
+ * It's also used to notify channels when an user join/exit a room.
65
+ *
66
+ * Map<roomId, Room>
67
+ */
68
+ this.rooms = new Map();
69
+ /**
70
+ * Current subscribing connections handled by the HotelClerk.
71
+ *
72
+ * Each connection can subscribe to many rooms with different volatile data.
73
+ *
74
+ * This object is used to keep track of all subscriptions made by a connection
75
+ * to be able to unsubscribe when a connection is removed.
76
+ *
77
+ * Map<connectionId, ConnectionRooms>
78
+ */
79
+ this.subscriptions = new Map();
80
+ this.module = realtimeModule;
81
+ this.koncorde = global.kuzzle.koncorde;
82
+ }
71
83
  /**
72
- * Number of created rooms. Used with the "subscriptionRooms"
73
- * configuration limit
84
+ * Registers the ask events.
74
85
  */
75
- this.roomsCount = 0;
76
-
86
+ async init() {
87
+ /**
88
+ * Create a new, empty room.
89
+ * @param {string} index
90
+ * @param {string} collection
91
+ * @param {string} roomId
92
+ * @returns {boolean} status indicating if the room was created or not
93
+ */
94
+ global.kuzzle.onAsk('core:realtime:room:create', (index, collection, roomId) => this.newRoom(index, collection, roomId));
95
+ /**
96
+ * Joins an existing room.
97
+ * @param {Request} request
98
+ * @returns {Promise}
99
+ */
100
+ global.kuzzle.onAsk('core:realtime:join', request => this.join(request));
101
+ /**
102
+ * Return the list of index, collection, rooms (+ their users count)
103
+ * on all index/collection pairs that the requesting user is allowed to
104
+ * subscribe
105
+ *
106
+ * @param {User} user
107
+ * @return {number}
108
+ * @throws {NotFoundError} If the roomId does not exist
109
+ */
110
+ global.kuzzle.onAsk('core:realtime:list', user => this.list(user));
111
+ /**
112
+ * Given an index, returns the list of collections having subscriptions
113
+ * on them.
114
+ * @param {string} index
115
+ * @return {Array.<string>}
116
+ */
117
+ global.kuzzle.onAsk('core:realtime:collections:get', index => this.listCollections(index));
118
+ /**
119
+ * Removes a user and all their subscriptions.
120
+ * @param {string} connectionId
121
+ */
122
+ global.kuzzle.onAsk('core:realtime:connection:remove', connectionId => this.removeConnection(connectionId));
123
+ /**
124
+ * Adds a new user subscription
125
+ * @param {Request} request
126
+ * @return {Object|null}
127
+ */
128
+ global.kuzzle.onAsk('core:realtime:subscribe', request => this.subscribe(request));
129
+ /**
130
+ * Unsubscribes a user from a room
131
+ * @param {string} connectionId
132
+ * @param {string} roomId
133
+ * @param {string} kuid
134
+ * @param {boolean} [notify]
135
+ */
136
+ global.kuzzle.onAsk('core:realtime:unsubscribe', (connectionId, roomId, notify) => {
137
+ return this.unsubscribe(connectionId, roomId, notify);
138
+ });
139
+ /**
140
+ * Clear the hotel clerk and properly disconnect connections.
141
+ */
142
+ global.kuzzle.on('kuzzle:shutdown', () => this.clearConnections());
143
+ /**
144
+ * Clear subscriptions when a connection is dropped
145
+ */
146
+ global.kuzzle.on('connection:remove', connection => {
147
+ this.removeConnection(connection.id)
148
+ .catch(err => global.kuzzle.log.info(err));
149
+ });
150
+ }
77
151
  /**
78
- * A simple list of rooms, containing their associated filter and how many
79
- * users have subscribed to it
80
- *
81
- * Example: subscribing to a chat room where the subject is Kuzzle
82
- * rooms = Map<roomId, room>
83
- *
84
- * Where:
85
- * - the room ID is the filter ID (e.g. 'f45de4d8ef4f3ze4ffzer85d4fgkzm41')
86
- * - room is an object with the following properties:
87
- * {
88
- * // list of users subscribing to this room
89
- * customers: Set([ 'connectionId' ]),
90
- *
91
- * // room channels
92
- * channels: {
93
- *
94
- * // channel ID
95
- * 'roomId-<configurationHash>': {
96
- *
97
- * // request scope filter, default: 'all'
98
- * scope: 'all|in|out|none',
152
+ * Subscribe a connection to a realtime room.
99
153
  *
100
- * // filter users notifications, default: 'none'
101
- * users: 'all|in|out|none',
154
+ * The room will be created if it does not already exists.
102
155
  *
103
- * // should propagate notification to the cluster
104
- * // (used for plugin subscriptions)
105
- * cluster: true|false
106
- * }
107
- * },
108
- * index: 'index',
109
- * collection: 'collection',
110
- * // the room unique identifier
111
- * id: 'id',
112
- * }
113
- * }
114
- */
115
- this.rooms = new Map();
116
-
117
- /**
118
- * In addition to this.rooms, this.customers allows managing users and their rooms
119
- * Example for a customer who subscribes to the room 'chat-room-kuzzle'
120
- * customers = Map.<connection id, customer rooms>
156
+ * Notify other subscribers on this room about this new subscription
121
157
  *
122
- * Where a customer room is an object with the following properties:
123
- * Map.<customer ID, Map.<room Id, volatile data>>
158
+ * @throws Throws if the user has already subscribed to this room name
159
+ * (just for rooms with same name, there is no error if the room
160
+ * has a different name with same filter) or if there is an error
161
+ * during room creation
124
162
  */
125
- this.customers = new Map();
126
- }
127
-
128
- async init () {
163
+ async subscribe(request) {
164
+ const { index, collection } = request.input.resource;
165
+ if (!index) {
166
+ return kerror_1.default.reject('api', 'assert', 'missing_argument', 'index');
167
+ }
168
+ if (!collection) {
169
+ return kerror_1.default.reject('api', 'assert', 'missing_argument', 'collection');
170
+ }
171
+ /*
172
+ * /!\ This check is a duplicate to the one already made by the
173
+ * funnel controller. THIS IS INTENTIONAL.
174
+ *
175
+ * This is to prevent subscriptions to be made on dead
176
+ * connections. And between the funnel and here, there is
177
+ * time for a connection to drop, so while the check
178
+ * on the funnel is useful for many use cases, this one
179
+ * is made on the very last moment and is essential to ensure
180
+ * that no zombie subscription can be performed
181
+ */
182
+ if (!global.kuzzle.router.isConnectionAlive(request.context)) {
183
+ return null;
184
+ }
185
+ let normalized;
186
+ try {
187
+ normalized = this.koncorde.normalize(request.input.body, (0, koncordeCompat_1.toKoncordeIndex)(index, collection));
188
+ }
189
+ catch (e) {
190
+ throw kerror_1.default.get('api', 'assert', 'koncorde_dsl_error', e.message);
191
+ }
192
+ this.createRoom(normalized);
193
+ const { channel, subscribed } = await this.subscribeToRoom(normalized.id, request);
194
+ if (subscribed) {
195
+ global.kuzzle.emit('core:realtime:subscribe:after', normalized.id);
196
+ // @deprecated -- to be removed in next major version
197
+ // we have to recreate the old "diff" object
198
+ await global.kuzzle.pipe('core:hotelClerk:addSubscription', {
199
+ changed: subscribed,
200
+ collection,
201
+ connectionId: request.context.connection.id,
202
+ filters: normalized.filter,
203
+ index,
204
+ roomId: normalized.id,
205
+ });
206
+ }
207
+ const subscription = new subscription_1.Subscription(index, collection, request.input.body, normalized.id, request.context.connection.id, request.context.user);
208
+ global.kuzzle.emit('core:realtime:user:subscribe:after', subscription);
209
+ return {
210
+ channel,
211
+ roomId: normalized.id,
212
+ };
213
+ }
129
214
  /**
130
- * Create a new, empty room.
131
- * @param {string} index
132
- * @param {string} collection
133
- * @param {string} roomId
134
- * @returns {boolean} status indicating if the room was created or not
215
+ * Returns the list of collections of an index with realtime rooms.
135
216
  */
136
- global.kuzzle.onAsk(
137
- 'core:realtime:room:create',
138
- (index, collection, roomId) => this.newRoom(index, collection, roomId));
139
-
217
+ listCollections(index) {
218
+ return (0, koncordeCompat_1.getCollections)(this.koncorde, index);
219
+ }
140
220
  /**
141
- * Joins an existing room.
142
- * @param {Request} request
143
- * @returns {Promise}
221
+ * Joins an existing realtime room.
222
+ *
223
+ * The room may exists on another cluster node, if it's the case, the normalized
224
+ * filters will be fetched from the cluster.
144
225
  */
145
- global.kuzzle.onAsk(
146
- 'core:realtime:join', request => this.join(request));
147
-
226
+ async join(request) {
227
+ const roomId = request.input.body.roomId;
228
+ if (!this.rooms.has(roomId)) {
229
+ const normalized = await global.kuzzle.ask('cluster:realtime:filters:get', roomId);
230
+ if (!normalized) {
231
+ throw realtimeError.get('room_not_found', roomId);
232
+ }
233
+ this.createRoom(normalized);
234
+ }
235
+ const { channel, cluster, subscribed } = await this.subscribeToRoom(roomId, request);
236
+ if (cluster && subscribed) {
237
+ global.kuzzle.emit('core:realtime:subscribe:after', roomId);
238
+ }
239
+ return {
240
+ channel,
241
+ roomId,
242
+ };
243
+ }
148
244
  /**
149
- * Return the list of index, collection, rooms (+ their users count)
245
+ * Return the list of index, collection, rooms and subscribing connections
150
246
  * on all index/collection pairs that the requesting user is allowed to
151
- * subscribe
152
- *
153
- * @param {User} user
154
- * @return {number}
155
- * @throws {NotFoundError} If the roomId does not exist
247
+ * subscribe.
156
248
  */
157
- global.kuzzle.onAsk('core:realtime:list', user => this.list(user));
158
-
249
+ async list(user) {
250
+ // We need the room list from the cluster's full state, NOT the one stored
251
+ // in Koncorde: the latter also contains subscriptions created by the
252
+ // framework (or by plugins), and we don't want those to appear in the API
253
+ const fullStateRooms = await global.kuzzle.ask('cluster:realtime:room:list');
254
+ const isAllowedRequest = new request_1.KuzzleRequest({
255
+ action: 'subscribe',
256
+ controller: 'realtime',
257
+ }, {});
258
+ for (const [index, collections] of Object.entries(fullStateRooms)) {
259
+ isAllowedRequest.input.resource.index = index;
260
+ const toRemove = await bluebird_1.default.filter(Object.keys(collections), collection => {
261
+ isAllowedRequest.input.resource.collection = collection;
262
+ return !user.isActionAllowed(isAllowedRequest);
263
+ });
264
+ for (const collection of toRemove) {
265
+ delete fullStateRooms[index][collection];
266
+ }
267
+ }
268
+ return fullStateRooms;
269
+ }
159
270
  /**
160
- * Given an index, returns the list of collections having subscriptions
161
- * on them.
162
- * @param {string} index
163
- * @return {Array.<string>}
271
+ * Removes a connections and unsubscribe it from every subscribed rooms.
272
+ *
273
+ * Usually called when an user has been disconnected from Kuzzle.
164
274
  */
165
- global.kuzzle.onAsk(
166
- 'core:realtime:collections:get',
167
- index => this.listCollections(index));
168
-
275
+ async removeConnection(connectionId, notify = true) {
276
+ const connectionRooms = this.subscriptions.get(connectionId);
277
+ if (!connectionRooms) {
278
+ // No need to raise an error if the connection does not have room subscriptions
279
+ return;
280
+ }
281
+ await bluebird_1.default.map(connectionRooms.roomIds, (roomId) => (this.unsubscribe(connectionId, roomId, notify).catch(global.kuzzle.log.error)));
282
+ }
169
283
  /**
170
- * Removes a user and all their subscriptions.
171
- * @param {string} connectionId
284
+ * Clear all connections made to this node:
285
+ * - trigger appropriate core events
286
+ * - send user exit room notifications
172
287
  */
173
- global.kuzzle.onAsk(
174
- 'core:realtime:user:remove',
175
- connectionId => this.removeUser(connectionId));
176
-
288
+ async clearConnections() {
289
+ await bluebird_1.default.map(this.subscriptions.keys(), (connectionId) => (this.removeConnection(connectionId, false)));
290
+ }
177
291
  /**
178
- * Adds a new user subscription
179
- * @param {Request} request
180
- * @return {Object|null}
292
+ * Register a new subscription
293
+ * - save the subscription on the provided room with volatile data
294
+ * - add the connection to the list of active connections of the room
181
295
  */
182
- global.kuzzle.onAsk(
183
- 'core:realtime:subscribe',
184
- request => this.subscribe(request));
185
-
296
+ registerSubscription(connectionId, roomId, volatile) {
297
+ debug('Add room %s for connection %s', roomId, connectionId);
298
+ let connectionRooms = this.subscriptions.get(connectionId);
299
+ if (!connectionRooms) {
300
+ connectionRooms = new connectionRooms_1.ConnectionRooms();
301
+ this.subscriptions.set(connectionId, connectionRooms);
302
+ }
303
+ connectionRooms.addRoom(roomId, volatile);
304
+ this.rooms.get(roomId).addConnection(connectionId);
305
+ }
186
306
  /**
187
- * Unsubscribes a user from a room
188
- * @param {string} connectionId
189
- * @param {string} roomId
190
- * @param {string} kuid
191
- * @param {boolean} [notify]
307
+ * Create new room if needed
308
+ *
309
+ * @returns {void}
192
310
  */
193
- global.kuzzle.onAsk(
194
- 'core:realtime:unsubscribe',
195
- (connectionId, roomId, notify) => {
196
- return this.unsubscribe(connectionId, roomId, notify);
197
- });
198
- }
199
-
200
- /**
201
- * Link a user connection to a room.
202
- * Create a new room if one doesn't already exist.
203
- * Notify other subscribers on this room about this new subscription
204
- *
205
- * @param {Request} request
206
- * @return {Promise.<UserNotification|null>}
207
- * @throws Throws if the user has already subscribed to this room name
208
- * (just for rooms with same name, there is no error if the room
209
- * has a different name with same filter) or if there is an error
210
- * during room creation
211
- */
212
- async subscribe (request) {
213
- const { index, collection } = request.input.resource;
214
-
215
- if (! index) {
216
- return kerror.reject('api', 'assert', 'missing_argument', 'index');
311
+ createRoom(normalized) {
312
+ const { index: koncordeIndex, id: roomId } = normalized;
313
+ const { index, collection } = (0, koncordeCompat_1.fromKoncordeIndex)(koncordeIndex);
314
+ if (this.rooms.has(normalized.id)) {
315
+ return;
316
+ }
317
+ const roomsLimit = global.kuzzle.config.limits.subscriptionRooms;
318
+ if (roomsLimit > 0 && this.roomsCount >= roomsLimit) {
319
+ throw realtimeError.get('too_many_rooms');
320
+ }
321
+ this.koncorde.store(normalized);
322
+ global.kuzzle.emit('core:realtime:room:create:after', normalized);
323
+ // @deprecated -- to be removed in the next major version of kuzzle
324
+ global.kuzzle.emit('room:new', { collection, index, roomId });
325
+ /*
326
+ In some very rare cases, the room may have been created between
327
+ the beginning of the function executed at the end of normalize,
328
+ and this one
329
+
330
+ Before incrementing the rooms count, we have to make sure this
331
+ is not the case to ensure our counter is right
332
+ */
333
+ if (this.newRoom(index, collection, roomId)) {
334
+ this.roomsCount++;
335
+ }
217
336
  }
218
-
219
- if (! collection) {
220
- return kerror.reject('api', 'assert', 'missing_argument', 'collection');
221
- }
222
-
223
- /*
224
- * /!\ This check is a duplicate to the one already made by the
225
- * funnel controller. THIS IS INTENTIONAL.
337
+ /**
338
+ * Remove a connection from a room.
339
+ *
340
+ * Also delete the rooms if it was the last connection subscribing to it.
226
341
  *
227
- * This is to prevent subscriptions to be made on dead
228
- * connections. And between the funnel and here, there is
229
- * time for a connection to drop, so while the check
230
- * on the funnel is useful for many use cases, this one
231
- * is made on the very last moment and is essential to ensure
232
- * that no zombie subscription can be performed
233
342
  */
234
- if (! global.kuzzle.router.isConnectionAlive(request.context)) {
235
- return null;
236
- }
237
-
238
- let normalized;
239
-
240
- try {
241
- normalized = global.kuzzle.koncorde.normalize(
242
- request.input.body,
243
- toKoncordeIndex(index, collection));
244
- }
245
- catch (e) {
246
- throw kerror.get('api', 'assert', 'koncorde_dsl_error', e.message);
247
- }
248
-
249
- this._createRoom(normalized);
250
-
251
- const { channel, subscribed } = await this._subscribeToRoom(
252
- normalized.id,
253
- request);
254
-
255
- if (subscribed) {
256
- global.kuzzle.emit('core:realtime:subscribe:after', normalized.id);
257
-
258
- // @deprecated -- to be removed in next major version
259
- // we have to recreate the old "diff" object -_-
260
- await global.kuzzle.pipe('core:hotelClerk:addSubscription', {
261
- changed: subscribed,
262
- collection,
263
- connectionId: request.context.connection.id,
264
- filters: normalized.filter,
265
- index,
266
- roomId: normalized.id,
267
- });
268
- }
269
-
270
- const subscription = new Subscription(
271
- index,
272
- collection,
273
- request.input.body,
274
- normalized.id,
275
- request.context.connection.id,
276
- request.context.user);
277
-
278
- global.kuzzle.emit('core:realtime:user:subscribe:after', subscription);
279
-
280
- return {
281
- channel,
282
- roomId: normalized.id,
283
- };
284
- }
285
-
286
- /**
287
- * Given an index, returns an array of collections on which some filters are
288
- * registered
289
- * @param {string} index
290
- * @returns {Array.<string>}
291
- */
292
- listCollections (index) {
293
- return getCollections(global.kuzzle.koncorde, index);
294
- }
295
-
296
- /**
297
- * Joins an existing room.
298
- *
299
- * @param {Request} request
300
- * @returns {Promise.<Object>}
301
- */
302
- async join (request) {
303
- const roomId = request.input.body.roomId;
304
-
305
- if (! this.rooms.has(roomId)) {
306
- const normalized = await global.kuzzle.ask(
307
- 'cluster:realtime:filters:get',
308
- roomId);
309
-
310
- if (!normalized) {
311
- throw realtimeError.get('room_not_found', roomId);
312
- }
313
-
314
- this._createRoom(normalized);
315
- }
316
-
317
- const response = await this._subscribeToRoom(roomId, request);
318
-
319
- if (response.cluster && response.subscribed) {
320
- global.kuzzle.emit('core:realtime:subscribe:after', roomId);
321
- }
322
-
323
- return {
324
- channel: response.channel,
325
- roomId,
326
- };
327
- }
328
-
329
- /**
330
- * Return the list of index, collection, rooms (+ their number of subscribers)
331
- * on all index/collection pairs that the requesting user is allowed to
332
- * subscribe
333
- *
334
- * Returned object looks like this:
335
- * {
336
- * <index>: {
337
- * <collection>: {
338
- * <roomId>: <number of subscribers>
339
- * }
340
- * }
341
- * }
342
- *
343
- * @param {User} user
344
- * @returns {Promise.<Object>} resolve an object listing all rooms subscribed
345
- * by the connected user
346
- */
347
- async list (user) {
348
- // We need the room list from the cluster's full state, NOT the one stored
349
- // in Koncorde: the latter also contains subscriptions created by the
350
- // framework (or by plugins), and we don't want those to appear in the API
351
- const list = await global.kuzzle.ask('cluster:realtime:room:list');
352
-
353
- const isAllowedRequest = new Request({
354
- action: 'subscribe',
355
- controller: 'realtime',
356
- });
357
-
358
- for (const index of Object.keys(list)) {
359
- isAllowedRequest.input.resource.index = index;
360
-
361
- const toRemove = await Bluebird.filter(
362
- Object.keys(list[index]),
363
- collection => {
364
- isAllowedRequest.input.resource.collection = collection;
365
-
366
- return !user.isActionAllowed(isAllowedRequest);
343
+ async unsubscribe(connectionId, roomId, notify = true) {
344
+ const connectionRooms = this.subscriptions.get(connectionId);
345
+ const requestContext = new request_1.RequestContext({
346
+ connection: { id: connectionId }
347
+ });
348
+ if (!connectionRooms) {
349
+ throw realtimeError.get('not_subscribed', connectionId, roomId);
350
+ }
351
+ const volatile = connectionRooms.getVolatile(roomId);
352
+ if (volatile === undefined) {
353
+ throw realtimeError.get('not_subscribed', connectionId, roomId);
354
+ }
355
+ if (connectionRooms.count > 1) {
356
+ connectionRooms.removeRoom(roomId);
357
+ }
358
+ else {
359
+ this.subscriptions.delete(connectionId);
360
+ }
361
+ const room = this.rooms.get(roomId);
362
+ if (!room) {
363
+ global.kuzzle.log.error(`Cannot remove room "${roomId}": room not found`);
364
+ throw realtimeError.get('room_not_found', roomId);
365
+ }
366
+ for (const channel of Object.keys(room.channels)) {
367
+ global.kuzzle.entryPoint.leaveChannel(channel, connectionId);
368
+ }
369
+ room.removeConnection(connectionId);
370
+ if (room.size === 0) {
371
+ await this.removeRoom(roomId);
372
+ }
373
+ // even if the room is deleted for this node, another one may need the
374
+ // notification
375
+ const request = new request_1.Request({
376
+ action: 'unsubscribe',
377
+ collection: room.collection,
378
+ controller: 'realtime',
379
+ index: room.index,
380
+ volatile,
381
+ }, requestContext);
382
+ await this.module.notifier.notifyUser(roomId, request, 'out', { count: room.size });
383
+ // Do not send an unsubscription notification if the room has been destroyed
384
+ // @aschen Why ?
385
+ if (notify
386
+ && this.rooms.has(roomId)
387
+ && room.channels.size > 0) {
388
+ await global.kuzzle.pipe('core:realtime:unsubscribe:after', roomId);
389
+ // @deprecated -- to be removed in next major version
390
+ await global.kuzzle.pipe('core:hotelClerk:removeRoomForCustomer', {
391
+ requestContext,
392
+ room: {
393
+ collection: room.collection,
394
+ id: roomId,
395
+ index: room.index,
396
+ },
397
+ });
398
+ }
399
+ const kuid = global.kuzzle.tokenManager.getKuidFromConnection(connectionId);
400
+ const subscription = new subscription_1.Subscription(room.index, room.collection, undefined, roomId, connectionId, { _id: kuid });
401
+ global.kuzzle.emit('core:realtime:user:unsubscribe:after', {
402
+ /* @deprecated */
403
+ requestContext,
404
+ /* @deprecated */
405
+ room: {
406
+ collection: room.collection,
407
+ id: roomId,
408
+ index: room.index,
409
+ },
410
+ subscription,
367
411
  });
368
-
369
- for (const collection of toRemove) {
370
- delete list[index][collection];
371
- }
372
- }
373
-
374
- return list;
375
- }
376
-
377
- /**
378
- * This function will delete a user from this.customers, and
379
- * decrement the subscribers count in all rooms where he has subscribed to
380
- * Usually called on a user disconnection event
381
- *
382
- * @param {string} connectionId
383
- */
384
- async removeUser (connectionId) {
385
- const customer = this.customers.get(connectionId);
386
-
387
- if (!customer) {
388
- // No need to raise an error if the connection has already been cleaned up
389
- return;
390
- }
391
-
392
- await Bluebird.map(customer.keys(), roomId => {
393
- return this.unsubscribe(connectionId, roomId)
394
- .catch(err => global.kuzzle.log.error(err));
395
- });
396
- }
397
-
398
- /**
399
- * Associate the room to the connection id in this.clients
400
- * Allow to manage later disconnection and delete socket/rooms/...
401
- *
402
- * @param {string} connectionId
403
- * @param {string} roomId
404
- * @param {object} volatile
405
- */
406
- _addRoomForCustomer (connectionId, roomId, volatile) {
407
- debug('Add room %s for customer %s', roomId, connectionId);
408
-
409
- let customer = this.customers.get(connectionId);
410
-
411
- if (! customer) {
412
- customer = new Map();
413
- this.customers.set(connectionId, customer);
414
- }
415
-
416
- this.rooms.get(roomId).customers.add(connectionId);
417
- customer.set(roomId, volatile);
418
- }
419
-
420
- /**
421
- * Create new room if needed
422
- *
423
- * @this HotelClerk
424
- *
425
- * @param {NormalizedFilter} normalized - Obtained with Koncorde.normalize
426
- * @param {Object} [options]
427
- *
428
- * @returns {void}
429
- */
430
- _createRoom (normalized) {
431
- const { index: koncordeIndex, id: roomId } = normalized;
432
- const [index, collection] = koncordeIndex.split('/');
433
-
434
- if (this.rooms.has(normalized.id)) {
435
- return;
436
- }
437
-
438
- const roomsLimit = global.kuzzle.config.limits.subscriptionRooms;
439
-
440
- if ( roomsLimit > 0 && this.roomsCount >= roomsLimit ) {
441
- throw realtimeError.get('too_many_rooms');
442
- }
443
-
444
- global.kuzzle.koncorde.store(normalized);
445
-
446
- global.kuzzle.emit('core:realtime:room:create:after', normalized);
447
-
448
- // @deprecated -- to be removed in the next major version of kuzzle
449
- global.kuzzle.emit('room:new', { collection, index, roomId });
450
-
451
- /*
452
- In some very rare cases, the room may have been created between
453
- the beginning of the function executed at the end of normalize,
454
- and this one
455
-
456
- Before incrementing the rooms count, we have to make sure this
457
- is not the case to ensure our counter is right
458
- */
459
- if (this.newRoom(index, collection, roomId)) {
460
- this.roomsCount++;
461
- }
462
- }
463
-
464
- /**
465
- * Remove the room from subscribed room from the user
466
- * Return the roomId in user mapping
467
- *
468
- * @this HotelClerk
469
- * @param {string} connectionId
470
- * @param {string} roomId
471
- * @param {Boolean} [notify]
472
- * @returns {Promise}
473
- */
474
- async unsubscribe (connectionId, roomId, notify = true) {
475
- const customer = this.customers.get(connectionId);
476
- const requestContext = new RequestContext({
477
- connection: { id: connectionId }
478
- });
479
-
480
- if (! customer) {
481
- throw realtimeError.get('not_subscribed', connectionId, roomId);
482
- }
483
-
484
- const volatile = customer.get(roomId);
485
-
486
- if (volatile === undefined) {
487
- throw realtimeError.get('not_subscribed', connectionId, roomId);
488
- }
489
-
490
- if (customer.size > 1) {
491
- customer.delete(roomId);
492
- }
493
- else {
494
- this.customers.delete(connectionId);
495
- }
496
-
497
- const room = this.rooms.get(roomId);
498
-
499
- if (! room) {
500
- global.kuzzle.log.error(`[hotelClerk] Cannot remove room "${roomId}": room not found`);
501
- throw realtimeError.get('room_not_found', roomId);
502
- }
503
-
504
- for (const channel of Object.keys(room.channels)) {
505
- global.kuzzle.entryPoint.leaveChannel(channel, connectionId);
506
- }
507
-
508
- if (room.customers.size === 1) {
509
- this.roomsCount--;
510
- this.rooms.delete(roomId);
511
-
512
- await this._removeRoomFromRealtimeEngine(roomId);
513
-
514
- room.customers = new Set();
515
- }
516
- else {
517
- room.customers.delete(connectionId);
518
- }
519
-
520
- // even if the room is deleted for this node, another one may need the
521
- // notification
522
- const request = new Request(
523
- {
524
- action: 'unsubscribe',
525
- collection: room.collection,
526
- controller: 'realtime',
527
- index: room.index,
528
- volatile,
529
- },
530
- requestContext);
531
-
532
- await this.module.notifier.notifyUser(roomId, request, 'out', {
533
- count: room.customers.size
534
- });
535
-
536
- // Do not send an unsubscription notification if the room has been destroyed
537
- if ( notify
538
- && this.rooms.has(roomId)
539
- && Object.keys(room.channels).length > 0
540
- ) {
541
- await global.kuzzle.pipe('core:realtime:unsubscribe:after', roomId);
542
-
543
- // @deprecated -- to be removed in next major version
544
- await global.kuzzle.pipe('core:hotelClerk:removeRoomForCustomer', {
545
- requestContext,
546
- room: {
547
- collection: room.collection,
548
- id: roomId,
549
- index: room.index,
550
- },
551
- });
552
- }
553
-
554
- const kuid = global.kuzzle.tokenManager.getKuidFromConnection(connectionId);
555
-
556
- const subscription = new Subscription(
557
- room.index,
558
- room.collection,
559
- undefined,
560
- roomId,
561
- connectionId,
562
- { _id: kuid });
563
-
564
- global.kuzzle.emit('core:realtime:user:unsubscribe:after', {
565
- /* @deprecated */
566
- requestContext,
567
- /* @deprecated */
568
- room: {
569
- collection: room.collection,
570
- id: roomId,
571
- index: room.index,
572
- },
573
- subscription,
574
- });
575
- }
576
-
577
- /**
578
- * Deletes a room if no user has subscribed to it, and removes it also from the
579
- * real-time engine
580
- *
581
- * @param {string} roomId
582
- */
583
- async _removeRoomFromRealtimeEngine (roomId) {
584
- // @deprecated -- to be removed in the next major version
585
- try {
586
- await global.kuzzle.pipe('room:remove', roomId);
587
- }
588
- catch (e) {
589
- return;
590
- }
591
-
592
- // We have to ask the cluster to dispatch the room removal event.
593
- // The cluster will also remove the room from Koncorde if no other node
594
- // uses it.
595
- // (this node may have no subscribers on it, but other nodes might)
596
- await global.kuzzle.ask('cluster:realtime:room:remove', roomId);
597
- }
598
-
599
- /**
600
- * Subscribes a user to an existing room.
601
- *
602
- * @param {string} roomId
603
- * @param {Request} request
604
- * @returns {Promise.<Object>}
605
- */
606
- async _subscribeToRoom (roomId, request) {
607
- let subscribed = false;
608
- let notifyPromise;
609
- const channel = new Channel(roomId, request.input.args);
610
- const connectionId = request.context.connection.id;
611
- const customer = this.customers.get(connectionId);
612
- const room = this.rooms.get(roomId);
613
-
614
- if ( !customer || !customer.has(roomId)) {
615
- subscribed = true;
616
- this._addRoomForCustomer(connectionId, roomId, request.input.volatile);
617
-
618
- notifyPromise = this.module.notifier.notifyUser(
619
- roomId,
620
- request,
621
- 'in',
622
- { count: room.customers.size });
623
- }
624
- else {
625
- notifyPromise = Bluebird.resolve();
626
412
  }
627
-
628
- global.kuzzle.entryPoint.joinChannel(channel.name, connectionId);
629
-
630
- if (! room.channels[channel.name]) {
631
- room.channels[channel.name] = channel;
413
+ /**
414
+ * Deletes a room if no user has subscribed to it, and removes it also from the
415
+ * real-time engine
416
+ */
417
+ async removeRoom(roomId) {
418
+ this.roomsCount--;
419
+ this.rooms.delete(roomId);
420
+ // @deprecated -- to be removed in the next major version
421
+ try {
422
+ await global.kuzzle.pipe('room:remove', roomId);
423
+ }
424
+ catch (e) {
425
+ return;
426
+ }
427
+ // We have to ask the cluster to dispatch the room removal event.
428
+ // The cluster will also remove the room from Koncorde if no other node
429
+ // uses it.
430
+ // (this node may have no subscribers on it, but other nodes might)
431
+ await global.kuzzle.ask('cluster:realtime:room:remove', roomId);
632
432
  }
633
-
634
- await notifyPromise;
635
-
636
- return {
637
- channel: channel.name,
638
- cluster: channel.cluster,
639
- subscribed,
640
- };
641
- }
642
-
643
- /**
644
- * Return the rooms a user has subscribed to.
645
- * @param {connectionId} connectionId
646
- * @returns {Array.<string>}
647
- */
648
- getUserRooms (connectionId) {
649
- const rooms = this.customers.get(connectionId);
650
-
651
- if (rooms) {
652
- return Array.from(rooms.keys());
433
+ /**
434
+ * Subscribes a connection to an existing room.
435
+ *
436
+ * The subscription is made on a configuration channel who will be created
437
+ * on the room if it does not already exists.
438
+ *
439
+ */
440
+ async subscribeToRoom(roomId, request) {
441
+ let subscribed = false;
442
+ let notifyPromise;
443
+ const { scope, users, propagate } = request.input.args;
444
+ const connectionId = request.context.connection.id;
445
+ const channel = new channel_1.Channel(roomId, { propagate, scope, users });
446
+ const connectionRooms = this.subscriptions.get(connectionId);
447
+ const room = this.rooms.get(roomId);
448
+ if (!connectionRooms || !connectionRooms.hasRoom(roomId)) {
449
+ subscribed = true;
450
+ this.registerSubscription(connectionId, roomId, request.input.volatile);
451
+ notifyPromise = this.module.notifier.notifyUser(roomId, request, 'in', { count: room.size });
452
+ }
453
+ else {
454
+ notifyPromise = bluebird_1.default.resolve();
455
+ }
456
+ global.kuzzle.entryPoint.joinChannel(channel.name, connectionId);
457
+ room.createChannel(channel);
458
+ await notifyPromise;
459
+ return {
460
+ channel: channel.name,
461
+ cluster: channel.cluster,
462
+ subscribed,
463
+ };
653
464
  }
654
-
655
- return [];
656
- }
657
-
658
- /**
659
- * Create an empty room in the RAM cache
660
- * @param {string} index
661
- * @param {string } collection
662
- * @param {string} roomId
663
- * @returns {boolean}
664
- */
665
- newRoom (index, collection, roomId) {
666
- if (!this.rooms.has(roomId)) {
667
- this.rooms.set(roomId, {
668
- channels: {},
669
- collection,
670
- customers: new Set(),
671
- id: roomId,
672
- index,
673
- });
674
-
675
- return true;
465
+ /**
466
+ * Create an empty room in the RAM cache if it doesn't exists
467
+ *
468
+ * @returns True if a new room has been created
469
+ */
470
+ newRoom(index, collection, roomId) {
471
+ if (!this.rooms.has(roomId)) {
472
+ this.rooms.set(roomId, new room_1.Room(roomId, index, collection));
473
+ return true;
474
+ }
475
+ return false;
676
476
  }
677
-
678
- return false;
679
- }
680
477
  }
681
-
682
- module.exports = HotelClerk;
478
+ exports.HotelClerk = HotelClerk;
479
+ //# sourceMappingURL=hotelClerk.js.map