Skip to content

Commit d10959d

Browse files
Merge branch 'develop' into feat/openapi-chat-postMessage
2 parents 73e85bf + c992f7d commit d10959d

File tree

9 files changed

+217
-75
lines changed

9 files changed

+217
-75
lines changed

apps/meteor/app/livechat/server/lib/QueueManager.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,10 @@ import { createLivechatRoom, createLivechatInquiry, allowAgentSkipQueue, prepare
2323
import { RoutingManager } from './RoutingManager';
2424
import { isVerifiedChannelInSource } from './contacts/isVerifiedChannelInSource';
2525
import { checkOnlineForDepartment } from './departmentsLib';
26-
import { afterInquiryQueued, afterRoomQueued, beforeDelegateAgent, onNewRoom } from './hooks';
26+
import { afterInquiryQueued, afterRoomQueued, beforeDelegateAgent, beforeRouteChat, onNewRoom } from './hooks';
2727
import { checkOnlineAgents, getOnlineAgents } from './service-status';
2828
import { getInquirySortMechanismSetting } from './settings';
2929
import { dispatchInquiryPosition } from '../../../../ee/app/livechat-enterprise/server/lib/Helper';
30-
import { callbacks } from '../../../../lib/callbacks';
3130
import { client, shouldRetryTransaction } from '../../../../server/database/utils';
3231
import { sendNotification } from '../../../lib/server';
3332
import { notifyOnLivechatInquiryChangedById, notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener';
@@ -43,13 +42,16 @@ export const saveQueueInquiry = async (inquiry: ILivechatInquiryRecord) => {
4342
return;
4443
}
4544

45+
// After inquiry queued does not modify the inquiry, its safe to return the return of queueInquiry
4646
await afterInquiryQueued(queuedInquiry);
4747

4848
void notifyOnLivechatInquiryChanged(queuedInquiry, 'updated', {
4949
status: LivechatInquiryStatus.QUEUED,
5050
queuedAt: new Date(),
5151
takenAt: undefined,
5252
});
53+
54+
return queuedInquiry;
5355
};
5456

5557
/**
@@ -99,7 +101,7 @@ export class QueueManager {
99101

100102
const inquiryAgent = await RoutingManager.delegateAgent(defaultAgent, inquiry);
101103
logger.debug(`Delegating inquiry with id ${inquiry._id} to agent ${defaultAgent?.username}`);
102-
const dbInquiry = await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent);
104+
const dbInquiry = await beforeRouteChat(inquiry, inquiryAgent);
103105

104106
if (!dbInquiry) {
105107
throw new Error('inquiry-not-found');

apps/meteor/app/livechat/server/lib/hooks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,9 @@ export const afterRoomQueued = makeFunction((room: IOmnichannelRoom) => {
132132

133133
return sendToCRM('LivechatSessionQueued', room);
134134
});
135+
136+
export const beforeRouteChat = makeFunction(
137+
async (inquiry: ILivechatInquiryRecord, _agent?: SelectedAgent | null): Promise<ILivechatInquiryRecord | null | undefined> => {
138+
return inquiry;
139+
},
140+
);
Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import type { IRole, IRoom, IUser } from '@rocket.chat/core-typings';
22
import mem from 'mem';
3-
import type { Filter } from 'mongodb';
43

54
import { CachedChatSubscription } from './CachedChatSubscription';
6-
import { Users } from './Users';
7-
import { isTruthy } from '../../../../lib/isTruthy';
85

96
/** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */
107
export const Subscriptions = Object.assign(CachedChatSubscription.collection, {
@@ -14,43 +11,10 @@ export const Subscriptions = Object.assign(CachedChatSubscription.collection, {
1411
return false;
1512
}
1613

17-
const query = {
18-
rid,
19-
};
20-
21-
const subscription = this.findOne(query, { fields: { roles: 1 } });
14+
const subscription = this.state.find((record) => record.rid === rid);
2215

2316
return subscription && Array.isArray(subscription.roles) && subscription.roles.includes(roleId);
2417
},
2518
{ maxAge: 1000, cacheKey: JSON.stringify },
2619
),
27-
28-
findUsersInRoles: mem(
29-
function (this: typeof CachedChatSubscription.collection, roles: IRole['_id'][] | IRole['_id'], scope?: string, options?: any) {
30-
roles = Array.isArray(roles) ? roles : [roles];
31-
32-
const query: Filter<any> = {
33-
roles: { $in: roles },
34-
};
35-
36-
if (scope) {
37-
query.rid = scope;
38-
}
39-
40-
const subscriptions = this.find(query).fetch();
41-
42-
const uids = subscriptions
43-
.map((subscription) => {
44-
if (typeof subscription.u !== 'undefined' && typeof subscription.u._id !== 'undefined') {
45-
return subscription.u._id;
46-
}
47-
48-
return undefined;
49-
})
50-
.filter(isTruthy);
51-
52-
return Users.find({ _id: { $in: uids } }, options);
53-
},
54-
{ maxAge: 1000, cacheKey: JSON.stringify },
55-
),
5620
});

apps/meteor/client/hooks/useUnread.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { useSession, useSessionDispatch, useUserPreference, useUserSubscriptions
33
import { useEffect } from 'react';
44

55
import { useFireGlobalEvent } from './useFireGlobalEvent';
6-
import { Rooms } from '../../app/models/client';
76

87
const query = { open: { $ne: false }, hideUnreadStatus: { $ne: true }, archived: { $ne: true } };
98
const options = { fields: { unread: 1, alert: 1, rid: 1, t: 1, name: 1, ls: 1, unreadAlert: 1, fname: 1, prid: 1 } };
@@ -23,11 +22,7 @@ export const useUnread = () => {
2322
let unreadAlert: false | '•' = false;
2423

2524
const unreadCount = subscriptions.reduce((ret, subscription) => {
26-
const room = Rooms.findOne({ _id: subscription.rid }, { fields: { usersCount: 1 } });
27-
fireEventUnreadChangedBySubscription({
28-
...subscription,
29-
usersCount: room?.usersCount,
30-
});
25+
fireEventUnreadChangedBySubscription(subscription);
3126

3227
if (subscription.alert || subscription.unread > 0) {
3328
// Increment the total unread count.

apps/meteor/client/lib/cachedCollections/DocumentMapStore.ts

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,171 @@ import { create } from 'zustand';
22

33
export interface IDocumentMapStore<T extends { _id: string }> {
44
readonly records: ReadonlyMap<T['_id'], T>;
5+
/**
6+
* Checks if a document with the given _id exists in the store.
7+
*
8+
* @param _id - The _id of the document to check.
9+
* @returns true if the document exists, false otherwise.
10+
*/
511
has(_id: T['_id']): boolean;
12+
/**
13+
* Retrieves a document by its _id.
14+
*
15+
* @param _id - The _id of the document to retrieve.
16+
* @returns The document if found, or undefined if not found.
17+
*/
618
get(_id: T['_id']): T | undefined;
19+
/**
20+
* Checks if any document in the store satisfies the given predicate.
21+
*
22+
* @param predicate - A function that takes a document and returns true if it matches the condition.
23+
* @returns true if at least one document matches the predicate, false otherwise.
24+
*/
725
some(predicate: (record: T) => boolean): boolean;
26+
/**
27+
* Finds a document that satisfies the given predicate.
28+
*
29+
* @param predicate - A function that takes a document and returns true if it matches the condition.
30+
* @returns The first document that matches the predicate, or undefined if no document matches.
31+
*/
832
find<U extends T>(predicate: (record: T) => record is U): U | undefined;
33+
/**
34+
* Finds a document that satisfies the given predicate.
35+
*
36+
* @param predicate - A function that takes a document and returns true if it matches the condition.
37+
* @returns The first document that matches the predicate, or undefined if no document matches.
38+
*/
939
find(predicate: (record: T) => boolean): T | undefined;
40+
/**
41+
* Finds the first document that satisfies the given predicate, using a comparator to determine the best match.
42+
*
43+
* Usually the "best" document is the first of a ordered set, but it can be any criteria defined by the comparator.
44+
*
45+
* @param predicate - A function that takes a document and returns true if it matches the condition.
46+
* @param comparator - A function that compares two documents and returns a negative number if the first is better, zero if they are equal, or a positive number if the second is better.
47+
* @returns The best matching document according to the predicate and comparator, or undefined if no document matches.
48+
*/
1049
findFirst<U extends T>(predicate: (record: T) => record is U, comparator: (a: T, b: T) => number): U | undefined;
50+
/**
51+
* Finds the first document that satisfies the given predicate, using a comparator to determine the best match.
52+
*
53+
* Usually the "best" document is the first of a ordered set, but it can be any criteria defined by the comparator.
54+
*
55+
* @param predicate - A function that takes a document and returns true if it matches the condition.
56+
* @param comparator - A function that compares two documents and returns a negative number if the first is better, zero if they are equal, or a positive number if the second is better.
57+
* @returns The best matching document according to the predicate and comparator, or undefined if no document matches.
58+
*/
1159
findFirst(predicate: (record: T) => boolean, comparator: (a: T, b: T) => number): T | undefined;
60+
/**
61+
* Filters documents in the store based on a predicate.
62+
*
63+
* @param predicate - A function that takes a document and returns true if it matches the condition.
64+
* @returns An array of documents that match the predicate.
65+
*/
1266
filter<U extends T>(predicate: (record: T) => record is U): U[];
67+
/**
68+
* Filters documents in the store based on a predicate.
69+
*
70+
* @param predicate - A function that takes a document and returns true if it matches the condition.
71+
* @returns An array of documents that match the predicate.
72+
*/
1373
filter(predicate: (record: T) => boolean): T[];
14-
indexBy<TKey extends keyof T>(key: TKey): Map<T[TKey], T>;
74+
/**
75+
* Creates an index of documents by a specified key.
76+
*
77+
* @param key - The key to index the documents by.
78+
* @returns A Map where the keys are the values of the specified key in the documents, and the values are the documents themselves.
79+
*/
80+
indexBy<TKey extends keyof T>(key: TKey): ReadonlyMap<T[TKey], T>;
81+
/**
82+
* Replaces all documents in the store with the provided records.
83+
*
84+
* @param records - An array of documents to replace the current records in the store.
85+
*/
1586
replaceAll(records: T[]): void;
87+
/**
88+
* Stores a single document in the store.
89+
*
90+
* @param doc - The document to store.
91+
*/
1692
store(doc: T): void;
93+
/**
94+
* Stores multiple documents in the store.
95+
*
96+
* @param docs - An iterable of documents to store.
97+
*/
1798
storeMany(docs: Iterable<T>): void;
99+
/**
100+
* Deletes a document from the store by its _id.
101+
*
102+
* @param _id - The _id of the document to delete.
103+
*/
18104
delete(_id: T['_id']): void;
105+
/**
106+
* Updates documents in the store that match a predicate.
107+
*
108+
* @param predicate - A function that takes a document and returns true if it matches the condition.
109+
* @param modifier - A function that takes a document and returns the modified document.
110+
* @returns void
111+
*/
19112
update<U extends T>(predicate: (record: T) => record is U, modifier: (record: U) => U): void;
113+
/**
114+
* Updates documents in the store that match a predicate.
115+
*
116+
* @param predicate - A function that takes a document and returns true if it matches the condition.
117+
* @param modifier - A function that takes a document and returns the modified document.
118+
* @returns void
119+
*/
20120
update(predicate: (record: T) => boolean, modifier: (record: T) => T): void;
121+
/**
122+
* Asynchronously updates documents in the store that match a predicate.
123+
*
124+
* @param predicate - A function that takes a document and returns true if it matches the condition.
125+
* @param modifier - A function that takes a document and returns a Promise that resolves to the modified document.
126+
* @returns void
127+
*/
21128
updateAsync<U extends T>(predicate: (record: T) => record is U, modifier: (record: U) => Promise<U>): Promise<void>;
129+
/**
130+
* Asynchronously updates documents in the store that match a predicate.
131+
*
132+
* @param predicate - A function that takes a document and returns true if it matches the condition.
133+
* @param modifier - A function that takes a document and returns a Promise that resolves to the modified document.
134+
* @returns void
135+
*/
22136
updateAsync(predicate: (record: T) => boolean, modifier: (record: T) => Promise<T>): Promise<void>;
137+
/**
138+
* Removes documents from the store that match a predicate.
139+
*
140+
* @param predicate - A function that takes a document and returns true if it matches the condition.
141+
*/
23142
remove(predicate: (record: T) => boolean): void;
24143
}
25144

145+
/**
146+
* Factory function to create a Zustand store that holds a map of documents.
147+
*
148+
* @param options - Optional callbacks to handle invalidation of documents.
149+
* @returns the Zustand store with methods to manage the document map.
150+
*/
26151
export const createDocumentMapStore = <T extends { _id: string }>({
27152
onInvalidate,
28153
onInvalidateAll,
29-
}: { onInvalidate?: (...docs: T[]) => void; onInvalidateAll?: () => void } = {}) =>
154+
}: {
155+
/**
156+
* Callback invoked when a document is stored, updated or deleted.
157+
*
158+
* This is useful to recompute Minimongo queries that depend on the changed documents.
159+
* @deprecated prefer subscribing to the store
160+
*/
161+
onInvalidate?: (...docs: T[]) => void;
162+
/**
163+
* Callback invoked when all documents are replaced in the store.
164+
*
165+
* This is useful to recompute Minimongo queries that depend on the changed documents.
166+
* @deprecated prefer subscribing to the store
167+
*/
168+
onInvalidateAll?: () => void;
169+
} = {}) =>
30170
create<IDocumentMapStore<T>>()((set, get) => ({
31171
records: new Map(),
32172
has: (id: T['_id']) => get().records.has(id),

apps/meteor/client/lib/cachedCollections/MinimongoCollection.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,61 @@ import { Mongo } from 'meteor/mongo';
22

33
import { createDocumentMapStore } from './DocumentMapStore';
44
import { LocalCollection } from './LocalCollection';
5+
import type { Query } from './Query';
56

67
/**
78
* Implements a minimal version of a MongoDB collection using Zustand for state management.
89
*
910
* It's a middle layer between the Mongo.Collection and Zustand aiming for complete migration to Zustand.
1011
*/
1112
export class MinimongoCollection<T extends { _id: string }> extends Mongo.Collection<T> {
13+
private pendingRecomputations = new Set<Query<T>>();
14+
15+
private recomputeAll() {
16+
this.pendingRecomputations.clear();
17+
18+
for (const query of this._collection.queries) {
19+
this._collection.recomputeQuery(query);
20+
}
21+
}
22+
23+
private scheduleRecomputationsFor(docs: T[]) {
24+
for (const query of this._collection.queries) {
25+
if (this.pendingRecomputations.has(query)) continue;
26+
27+
if (docs.some((doc) => query.predicate(doc))) {
28+
this.scheduleRecomputation(query);
29+
}
30+
}
31+
}
32+
33+
private scheduleRecomputation(query: Query<T>) {
34+
this.pendingRecomputations.add(query);
35+
36+
queueMicrotask(() => {
37+
if (this.pendingRecomputations.size === 0) return;
38+
39+
this.pendingRecomputations.forEach((query) => {
40+
this._collection.recomputeQuery(query);
41+
});
42+
this.pendingRecomputations.clear();
43+
});
44+
}
45+
1246
/**
1347
* A Zustand store that holds the records of the collection.
1448
*
1549
* It should be used as a hook in React components to access the collection's records and methods.
50+
*
51+
* Beware mutating the store will **asynchronously** trigger recomputations of all Minimongo
52+
* queries that depend on the changed documents.
1653
*/
1754
readonly use = createDocumentMapStore<T>({
18-
onInvalidate: (...docs) => {
19-
for (const query of this._collection.queries) {
20-
if (docs.some((doc) => query.predicate(doc))) {
21-
this._collection.recomputeQuery(query);
22-
}
23-
}
24-
},
2555
onInvalidateAll: () => {
26-
for (const query of this._collection.queries) {
27-
this._collection.recomputeQuery(query);
28-
}
56+
this.recomputeAll();
57+
},
58+
onInvalidate: (...docs) => {
59+
this.scheduleRecomputationsFor(docs);
2960
},
3061
});
3162

@@ -44,6 +75,9 @@ export class MinimongoCollection<T extends { _id: string }> extends Mongo.Collec
4475
* Returns the Zustand store state that holds the records of the collection.
4576
*
4677
* It's a convenience method to access the Zustand store directly i.e. outside of React components.
78+
*
79+
* Beware mutating the store will **asynchronously** trigger recomputations of all Minimongo
80+
* queries that depend on the changed documents.
4781
*/
4882
get state() {
4983
return this.use.getState();

0 commit comments

Comments
 (0)