@tiledesk/tiledesk-server 2.27.1 → 2.28.0
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/CHANGELOG.md +9 -0
- package/channels/chat21/chat21Handler.js +142 -29
- package/channels/chat21/package-lock.json +70 -32
- package/channels/chat21/package.json +1 -1
- package/package.json +1 -1
- package/services/chat21SyncTracer.js +69 -19
- package/services/requestService.js +24 -13
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
🚀 IN PRODUCTION 🚀
|
|
6
6
|
(https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
|
|
7
7
|
|
|
8
|
+
# 2.28.0
|
|
9
|
+
- Group participant updates (setMembers, join, leave) for the same request are now queued and executed sequentially to prevent race conditions.
|
|
10
|
+
- setMembers now reloads participants from the database at the time of execution, ensuring consistency with the actual request state.
|
|
11
|
+
- Automatic retries for setMembers on error, with configurable max retries and delay via CHAT21_SET_MEMBERS_MAX_RETRIES and CHAT21_SET_MEMBERS_RETRY_DELAY_MS.
|
|
12
|
+
- Removed participants’ conversations are now archived only after leave/setMembers completes, inside the same queue, to avoid premature archival while the participant is still in the Chat21 group.
|
|
13
|
+
|
|
14
|
+
# 2.27.2
|
|
15
|
+
- Improved chat21 sync tracing: traces are buffered in memory and saved in batches (insertMany) instead of one insert per trace
|
|
16
|
+
|
|
8
17
|
# 2.27.1
|
|
9
18
|
- Added tracing for chat21 events
|
|
10
19
|
|
|
@@ -42,7 +42,7 @@ function archiveConversation(event, request, user_id) {
|
|
|
42
42
|
|
|
43
43
|
return chat21SyncTracer.traceCall(event, request, 'archive', { user_id: user_id }, () => {
|
|
44
44
|
return chat21.conversations.archive(request.request_id, user_id);
|
|
45
|
-
}).then(function (data) {
|
|
45
|
+
}, chat21Outcome).then(function (data) {
|
|
46
46
|
winston.verbose("[Chat21Handler] - " + event + " - Chat21 conversation archived for " + user_id + " result " + JSON.stringify(data));
|
|
47
47
|
chat21Event.emit('conversation.archived', data);
|
|
48
48
|
}).catch(function (err) {
|
|
@@ -61,6 +61,103 @@ function runTraced(event, request, fn) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function nonNegativeNumber(value, defaultValue) {
|
|
65
|
+
const parsed = parseInt(value, 10);
|
|
66
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : defaultValue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const SET_MEMBERS_MAX_RETRIES = nonNegativeNumber(process.env.CHAT21_SET_MEMBERS_MAX_RETRIES, 2);
|
|
70
|
+
const SET_MEMBERS_RETRY_DELAY_MS = nonNegativeNumber(process.env.CHAT21_SET_MEMBERS_RETRY_DELAY_MS, 1000);
|
|
71
|
+
|
|
72
|
+
function isSameMembersResponse(response) {
|
|
73
|
+
const message = response && response.err && response.err.message;
|
|
74
|
+
return typeof message === 'string' && message.startsWith('Same members');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// chat21-http-server answers {success: true} on success: any other body is an error, even with a resolved sdk promise
|
|
78
|
+
function chat21Outcome(response) {
|
|
79
|
+
if (response && response.success === true) {
|
|
80
|
+
return 'success';
|
|
81
|
+
}
|
|
82
|
+
if (isSameMembersResponse(response)) {
|
|
83
|
+
return 'skipped';
|
|
84
|
+
}
|
|
85
|
+
return 'error';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const groupSyncQueues = new Map();
|
|
89
|
+
|
|
90
|
+
// Chains the chat21 group updates of the same request, so that this process sends them one at a time and in emission order
|
|
91
|
+
function enqueueGroupSync(groupId, fn) {
|
|
92
|
+
const previous = groupSyncQueues.get(groupId) || Promise.resolve();
|
|
93
|
+
const current = previous.then(fn);
|
|
94
|
+
const tail = current.catch(() => { });
|
|
95
|
+
groupSyncQueues.set(groupId, tail);
|
|
96
|
+
tail.then(() => {
|
|
97
|
+
if (groupSyncQueues.get(groupId) === tail) {
|
|
98
|
+
groupSyncQueues.delete(groupId);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
return current;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The participants are read when the call is sent, not taken from the event: a later update,
|
|
105
|
+
// possibly saved by another pod, would otherwise be overwritten on chat21 by stale members
|
|
106
|
+
async function currentGroupMembers(request) {
|
|
107
|
+
let participants = request.participants;
|
|
108
|
+
try {
|
|
109
|
+
const current = await Request.findOne({ request_id: request.request_id, id_project: request.id_project })
|
|
110
|
+
.select('participants')
|
|
111
|
+
.lean()
|
|
112
|
+
.exec();
|
|
113
|
+
if (current) {
|
|
114
|
+
participants = current.participants;
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
winston.error("[Chat21Handler] - Error reading the current participants, using the event ones for request " + request.request_id, err);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const members = ["system", ...participants];
|
|
121
|
+
if (request.lead) {
|
|
122
|
+
members.push(request.lead.lead_id);
|
|
123
|
+
}
|
|
124
|
+
return members;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function delay(ms) {
|
|
128
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function syncGroupMembers(event, request) {
|
|
132
|
+
const groupId = request.request_id;
|
|
133
|
+
|
|
134
|
+
for (let attempt = 0; ; attempt++) {
|
|
135
|
+
const members = await currentGroupMembers(request);
|
|
136
|
+
winston.verbose("[Chat21Handler] - " + event + " - Chat21 group " + groupId + " members (attempt " + attempt + "): ", members);
|
|
137
|
+
|
|
138
|
+
let error;
|
|
139
|
+
try {
|
|
140
|
+
chat21.auth.setAdminToken(adminToken);
|
|
141
|
+
const data = await chat21SyncTracer.traceCall(event, request, 'group.setMembers', { members: members, attempt: attempt }, () => {
|
|
142
|
+
return chat21.groups.setMembers(members, groupId);
|
|
143
|
+
}, chat21Outcome);
|
|
144
|
+
|
|
145
|
+
if (chat21Outcome(data) !== 'error') {
|
|
146
|
+
return data;
|
|
147
|
+
}
|
|
148
|
+
error = { message: "Chat21 setMembers failed", response: data };
|
|
149
|
+
} catch (err) {
|
|
150
|
+
error = err;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (attempt >= SET_MEMBERS_MAX_RETRIES) {
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
winston.warn("[Chat21Handler] - " + event + " - Chat21 setMembers failed for group " + groupId + " (attempt " + attempt + "), retrying: ", error);
|
|
157
|
+
await delay(SET_MEMBERS_RETRY_DELAY_MS * (attempt + 1));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
64
161
|
|
|
65
162
|
class Chat21Handler {
|
|
66
163
|
|
|
@@ -494,26 +591,19 @@ class Chat21Handler {
|
|
|
494
591
|
}
|
|
495
592
|
|
|
496
593
|
chat21.auth.setAdminToken(adminToken);
|
|
497
|
-
let requestObj = request.toJSON();
|
|
498
|
-
|
|
499
|
-
winston.verbose("[Chat21Handler] - request.participants.update - Setting chat21 group for request with id: " + requestObj._id);
|
|
500
|
-
|
|
501
|
-
const groupId = request.request_id;
|
|
502
|
-
|
|
503
|
-
let members = [];
|
|
504
|
-
members.push("system");
|
|
505
|
-
|
|
506
|
-
request.participants.forEach(function (participant, index) {
|
|
507
|
-
members.push(participant);
|
|
508
|
-
});
|
|
509
594
|
|
|
510
|
-
|
|
511
|
-
members.push(request.lead.lead_id);
|
|
512
|
-
}
|
|
513
|
-
winston.verbose("[Chat21Handler] - request.participants.update - Chat21 group with members for request.participants.update: ", members);
|
|
595
|
+
winston.verbose("[Chat21Handler] - request.participants.update - Setting chat21 group for request with id: " + request._id);
|
|
514
596
|
|
|
515
|
-
|
|
516
|
-
|
|
597
|
+
enqueueGroupSync(request.request_id, async () => {
|
|
598
|
+
try {
|
|
599
|
+
return await syncGroupMembers(traceEvent, request);
|
|
600
|
+
} finally {
|
|
601
|
+
// Archived only after leaving the chat21 group: until then the group messages (e.g. the
|
|
602
|
+
// TOUCHING_OPERATOR info sent on the same event) reach their timeline and set archived: false
|
|
603
|
+
removedParticipants.forEach(function (removedParticipant) {
|
|
604
|
+
archiveConversation(traceEvent, request, removedParticipant);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
517
607
|
}).then(function (data) {
|
|
518
608
|
winston.verbose("[Chat21Handler] - request.participants.update - Chat21 group set for request.participants.update : " + JSON.stringify(data));
|
|
519
609
|
chat21Event.emit('group.setMembers', data);
|
|
@@ -522,10 +612,6 @@ class Chat21Handler {
|
|
|
522
612
|
chat21Event.emit('group.setMembers.error', err);
|
|
523
613
|
});
|
|
524
614
|
|
|
525
|
-
removedParticipants.forEach(function (removedParticipant) {
|
|
526
|
-
archiveConversation(traceEvent, request, removedParticipant);
|
|
527
|
-
});
|
|
528
|
-
|
|
529
615
|
}));
|
|
530
616
|
});
|
|
531
617
|
|
|
@@ -550,9 +636,10 @@ class Chat21Handler {
|
|
|
550
636
|
const groupId = request.request_id;
|
|
551
637
|
winston.verbose("[Chat21Handler] - request.participants.join - Joining member " + member + " for chat21 group with request : " + groupId);
|
|
552
638
|
|
|
553
|
-
chat21SyncTracer.traceCall(traceEvent, request, 'group.join', { member: member }, () => {
|
|
639
|
+
enqueueGroupSync(groupId, () => chat21SyncTracer.traceCall(traceEvent, request, 'group.join', { member: member }, () => {
|
|
640
|
+
chat21.auth.setAdminToken(adminToken);
|
|
554
641
|
return chat21.groups.join(member, groupId);
|
|
555
|
-
}).then(function (data) {
|
|
642
|
+
}, chat21Outcome)).then(function (data) {
|
|
556
643
|
winston.verbose("[Chat21Handler] - request.participants.join - Chat21 group joined: " + JSON.stringify(data));
|
|
557
644
|
chat21Event.emit('group.join', data);
|
|
558
645
|
}).catch(function (err) {
|
|
@@ -584,8 +671,16 @@ class Chat21Handler {
|
|
|
584
671
|
|
|
585
672
|
winston.verbose("[Chat21Handler] - request.participants.leave - Leaving " + member + " for chat21 group for request with id: " + groupId);
|
|
586
673
|
|
|
587
|
-
|
|
588
|
-
|
|
674
|
+
enqueueGroupSync(groupId, async () => {
|
|
675
|
+
try {
|
|
676
|
+
return await chat21SyncTracer.traceCall(traceEvent, request, 'group.leave', { member: member }, () => {
|
|
677
|
+
chat21.auth.setAdminToken(adminToken);
|
|
678
|
+
return chat21.groups.leave(member, groupId);
|
|
679
|
+
}, chat21Outcome);
|
|
680
|
+
} finally {
|
|
681
|
+
// Archived only after leaving the chat21 group, see request.participants.update
|
|
682
|
+
archiveConversation(traceEvent, request, member);
|
|
683
|
+
}
|
|
589
684
|
}).then(function (data) {
|
|
590
685
|
winston.verbose("[Chat21Handler] - request.participants.leave - Chat21 group left: " + JSON.stringify(data));
|
|
591
686
|
chat21Event.emit('group.leave', data);
|
|
@@ -594,8 +689,6 @@ class Chat21Handler {
|
|
|
594
689
|
chat21Event.emit('group.leave.error', err);
|
|
595
690
|
});
|
|
596
691
|
|
|
597
|
-
archiveConversation(traceEvent, request, member);
|
|
598
|
-
|
|
599
692
|
}));
|
|
600
693
|
})
|
|
601
694
|
|
|
@@ -689,6 +782,26 @@ class Chat21Handler {
|
|
|
689
782
|
});
|
|
690
783
|
});
|
|
691
784
|
|
|
785
|
+
groupEvent.on('group.deleted', (request_id) => {
|
|
786
|
+
|
|
787
|
+
winston.debug("Chat21Handler on group.deleted for request_id: " + request_id);
|
|
788
|
+
|
|
789
|
+
setImmediate(() => {
|
|
790
|
+
|
|
791
|
+
chat21.auth.setAdminToken(adminToken);
|
|
792
|
+
|
|
793
|
+
return chat21.groups.delete(request_id).then(function (data) {
|
|
794
|
+
winston.verbose("[Chat21Handler] - group.deleted - Chat21 group deleted: " + JSON.stringify(data));
|
|
795
|
+
chat21Event.emit('group.deleted', data);
|
|
796
|
+
}).catch(function (err) {
|
|
797
|
+
winston.error("[Chat21Handler] - group.deleted - Error deleting chat21 group: ", err);
|
|
798
|
+
chat21Event.emit('group.deleted.error', err);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
})
|
|
804
|
+
|
|
692
805
|
}
|
|
693
806
|
|
|
694
807
|
}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiledesk/tiledesk-chat21-app",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"lockfileVersion": 2,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@tiledesk/tiledesk-chat21-app",
|
|
9
|
-
"version": "1.1.
|
|
9
|
+
"version": "1.1.8",
|
|
10
10
|
"license": "AGPL-3.0",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@chat21/chat21-node-sdk": "^1.1.
|
|
12
|
+
"@chat21/chat21-node-sdk": "^1.1.12",
|
|
13
13
|
"firebase-admin": "^9.5.0",
|
|
14
14
|
"winston": "^3.3.3"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {}
|
|
17
17
|
},
|
|
18
18
|
"node_modules/@chat21/chat21-node-sdk": {
|
|
19
|
-
"version": "1.1.
|
|
20
|
-
"resolved": "https://registry.npmjs.org/@chat21/chat21-node-sdk/-/chat21-node-sdk-1.1.
|
|
21
|
-
"integrity": "sha512-
|
|
19
|
+
"version": "1.1.12",
|
|
20
|
+
"resolved": "https://registry.npmjs.org/@chat21/chat21-node-sdk/-/chat21-node-sdk-1.1.12.tgz",
|
|
21
|
+
"integrity": "sha512-KllkQ4HckOAG/l6xIycCCNgM5Y/fDDfEFbK5eVCCMoBJsS4DGJoRB/Zh1Td792E/tdzvn/skTBpGA9+I/hXqGA==",
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"chai": "^4.3.0",
|
|
24
24
|
"dotenv": "^8.2.0",
|
|
@@ -199,15 +199,6 @@
|
|
|
199
199
|
"@firebase/app": "0.x"
|
|
200
200
|
}
|
|
201
201
|
},
|
|
202
|
-
"node_modules/@firebase/auth-interop-types": {
|
|
203
|
-
"version": "0.1.5",
|
|
204
|
-
"resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz",
|
|
205
|
-
"integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==",
|
|
206
|
-
"peerDependencies": {
|
|
207
|
-
"@firebase/app-types": "0.x",
|
|
208
|
-
"@firebase/util": "0.x"
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
202
|
"node_modules/@firebase/auth-types": {
|
|
212
203
|
"version": "0.10.3",
|
|
213
204
|
"resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.3.tgz",
|
|
@@ -261,6 +252,23 @@
|
|
|
261
252
|
"@firebase/app-types": "0.6.1"
|
|
262
253
|
}
|
|
263
254
|
},
|
|
255
|
+
"node_modules/@firebase/database/node_modules/@firebase/auth-interop-types": {
|
|
256
|
+
"version": "0.1.5",
|
|
257
|
+
"resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz",
|
|
258
|
+
"integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==",
|
|
259
|
+
"peerDependencies": {
|
|
260
|
+
"@firebase/app-types": "0.x",
|
|
261
|
+
"@firebase/util": "0.x"
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
"node_modules/@firebase/database/node_modules/@firebase/util": {
|
|
265
|
+
"version": "0.3.4",
|
|
266
|
+
"resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.4.tgz",
|
|
267
|
+
"integrity": "sha512-VwjJUE2Vgr2UMfH63ZtIX9Hd7x+6gayi6RUXaTqEYxSbf/JmehLmAEYSuxS/NckfzAXWeGnKclvnXVibDgpjQQ==",
|
|
268
|
+
"dependencies": {
|
|
269
|
+
"tslib": "^1.11.1"
|
|
270
|
+
}
|
|
271
|
+
},
|
|
264
272
|
"node_modules/@firebase/firestore": {
|
|
265
273
|
"version": "2.4.1",
|
|
266
274
|
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-2.4.1.tgz",
|
|
@@ -765,13 +773,24 @@
|
|
|
765
773
|
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
|
766
774
|
},
|
|
767
775
|
"node_modules/@firebase/util": {
|
|
768
|
-
"version": "
|
|
769
|
-
"resolved": "https://registry.npmjs.org/@firebase/util/-/util-
|
|
770
|
-
"integrity": "sha512-
|
|
776
|
+
"version": "1.15.3",
|
|
777
|
+
"resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.3.tgz",
|
|
778
|
+
"integrity": "sha512-c/z/gaIlaaLZEuGbE6sLUuJ61tskg1JghvhcNQzW948ASBinbVBBRnZTC4b4yt4LaEtJQkYlyzqLHcutFwEIvA==",
|
|
779
|
+
"hasInstallScript": true,
|
|
780
|
+
"peer": true,
|
|
771
781
|
"dependencies": {
|
|
772
|
-
"tslib": "^1.
|
|
782
|
+
"tslib": "^2.1.0"
|
|
783
|
+
},
|
|
784
|
+
"engines": {
|
|
785
|
+
"node": ">=20.0.0"
|
|
773
786
|
}
|
|
774
787
|
},
|
|
788
|
+
"node_modules/@firebase/util/node_modules/tslib": {
|
|
789
|
+
"version": "2.8.1",
|
|
790
|
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
|
791
|
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
|
792
|
+
"peer": true
|
|
793
|
+
},
|
|
775
794
|
"node_modules/@firebase/webchannel-wrapper": {
|
|
776
795
|
"version": "0.5.1",
|
|
777
796
|
"resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.5.1.tgz",
|
|
@@ -3645,9 +3664,9 @@
|
|
|
3645
3664
|
},
|
|
3646
3665
|
"dependencies": {
|
|
3647
3666
|
"@chat21/chat21-node-sdk": {
|
|
3648
|
-
"version": "1.1.
|
|
3649
|
-
"resolved": "https://registry.npmjs.org/@chat21/chat21-node-sdk/-/chat21-node-sdk-1.1.
|
|
3650
|
-
"integrity": "sha512-
|
|
3667
|
+
"version": "1.1.12",
|
|
3668
|
+
"resolved": "https://registry.npmjs.org/@chat21/chat21-node-sdk/-/chat21-node-sdk-1.1.12.tgz",
|
|
3669
|
+
"integrity": "sha512-KllkQ4HckOAG/l6xIycCCNgM5Y/fDDfEFbK5eVCCMoBJsS4DGJoRB/Zh1Td792E/tdzvn/skTBpGA9+I/hXqGA==",
|
|
3651
3670
|
"requires": {
|
|
3652
3671
|
"chai": "^4.3.0",
|
|
3653
3672
|
"dotenv": "^8.2.0",
|
|
@@ -3825,12 +3844,6 @@
|
|
|
3825
3844
|
"@firebase/auth-types": "0.10.3"
|
|
3826
3845
|
}
|
|
3827
3846
|
},
|
|
3828
|
-
"@firebase/auth-interop-types": {
|
|
3829
|
-
"version": "0.1.5",
|
|
3830
|
-
"resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz",
|
|
3831
|
-
"integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==",
|
|
3832
|
-
"requires": {}
|
|
3833
|
-
},
|
|
3834
3847
|
"@firebase/auth-types": {
|
|
3835
3848
|
"version": "0.10.3",
|
|
3836
3849
|
"resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.3.tgz",
|
|
@@ -3873,6 +3886,22 @@
|
|
|
3873
3886
|
"@firebase/util": "0.3.4",
|
|
3874
3887
|
"faye-websocket": "0.11.3",
|
|
3875
3888
|
"tslib": "^1.11.1"
|
|
3889
|
+
},
|
|
3890
|
+
"dependencies": {
|
|
3891
|
+
"@firebase/auth-interop-types": {
|
|
3892
|
+
"version": "0.1.5",
|
|
3893
|
+
"resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz",
|
|
3894
|
+
"integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==",
|
|
3895
|
+
"requires": {}
|
|
3896
|
+
},
|
|
3897
|
+
"@firebase/util": {
|
|
3898
|
+
"version": "0.3.4",
|
|
3899
|
+
"resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.4.tgz",
|
|
3900
|
+
"integrity": "sha512-VwjJUE2Vgr2UMfH63ZtIX9Hd7x+6gayi6RUXaTqEYxSbf/JmehLmAEYSuxS/NckfzAXWeGnKclvnXVibDgpjQQ==",
|
|
3901
|
+
"requires": {
|
|
3902
|
+
"tslib": "^1.11.1"
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3876
3905
|
}
|
|
3877
3906
|
},
|
|
3878
3907
|
"@firebase/database-types": {
|
|
@@ -4309,11 +4338,20 @@
|
|
|
4309
4338
|
"requires": {}
|
|
4310
4339
|
},
|
|
4311
4340
|
"@firebase/util": {
|
|
4312
|
-
"version": "
|
|
4313
|
-
"resolved": "https://registry.npmjs.org/@firebase/util/-/util-
|
|
4314
|
-
"integrity": "sha512-
|
|
4341
|
+
"version": "1.15.3",
|
|
4342
|
+
"resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.3.tgz",
|
|
4343
|
+
"integrity": "sha512-c/z/gaIlaaLZEuGbE6sLUuJ61tskg1JghvhcNQzW948ASBinbVBBRnZTC4b4yt4LaEtJQkYlyzqLHcutFwEIvA==",
|
|
4344
|
+
"peer": true,
|
|
4315
4345
|
"requires": {
|
|
4316
|
-
"tslib": "^1.
|
|
4346
|
+
"tslib": "^2.1.0"
|
|
4347
|
+
},
|
|
4348
|
+
"dependencies": {
|
|
4349
|
+
"tslib": {
|
|
4350
|
+
"version": "2.8.1",
|
|
4351
|
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
|
4352
|
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
|
4353
|
+
"peer": true
|
|
4354
|
+
}
|
|
4317
4355
|
}
|
|
4318
4356
|
},
|
|
4319
4357
|
"@firebase/webchannel-wrapper": {
|
package/package.json
CHANGED
|
@@ -6,12 +6,55 @@ const MAX_STRING_LENGTH = 2000;
|
|
|
6
6
|
const enabled = process.env.CHAT21_SYNC_TRACE_ENABLED === "true";
|
|
7
7
|
const host = os.hostname();
|
|
8
8
|
|
|
9
|
+
function positiveNumber(value, defaultValue) {
|
|
10
|
+
const parsed = parseInt(value, 10);
|
|
11
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const FLUSH_INTERVAL_MS = positiveNumber(process.env.CHAT21_SYNC_TRACE_FLUSH_INTERVAL_MS, 1000);
|
|
15
|
+
const BATCH_SIZE = positiveNumber(process.env.CHAT21_SYNC_TRACE_BATCH_SIZE, 200);
|
|
16
|
+
const MAX_BUFFER_SIZE = positiveNumber(process.env.CHAT21_SYNC_TRACE_MAX_BUFFER_SIZE, 10000);
|
|
17
|
+
|
|
18
|
+
let buffer = [];
|
|
19
|
+
let flushing = false;
|
|
20
|
+
let dropped = 0;
|
|
21
|
+
|
|
9
22
|
if (enabled) {
|
|
10
|
-
winston.info("[Chat21SyncTracer] Chat21 sync tracing enabled"
|
|
23
|
+
winston.info("[Chat21SyncTracer] Chat21 sync tracing enabled. Flush interval (ms): " + FLUSH_INTERVAL_MS +
|
|
24
|
+
" batch size: " + BATCH_SIZE + " max buffer size: " + MAX_BUFFER_SIZE);
|
|
11
25
|
// The TTL index must exist even when MONGOOSE_AUTOINDEX is false
|
|
12
26
|
Chat21SyncTrace.createIndexes().catch((err) => {
|
|
13
27
|
winston.error("[Chat21SyncTracer] Error creating indexes: ", err);
|
|
14
28
|
});
|
|
29
|
+
setInterval(flush, FLUSH_INTERVAL_MS).unref();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Writes the buffered traces in batches. Only one flush runs at a time: while mongo is slow
|
|
34
|
+
* the buffer grows up to MAX_BUFFER_SIZE, then new traces are dropped.
|
|
35
|
+
* @returns {Promise<void>} never rejects
|
|
36
|
+
*/
|
|
37
|
+
async function flush() {
|
|
38
|
+
if (flushing || buffer.length === 0) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
flushing = true;
|
|
42
|
+
try {
|
|
43
|
+
if (dropped > 0) {
|
|
44
|
+
winston.warn("[Chat21SyncTracer] Buffer full, dropped " + dropped + " traces");
|
|
45
|
+
dropped = 0;
|
|
46
|
+
}
|
|
47
|
+
while (buffer.length > 0) {
|
|
48
|
+
const batch = buffer.splice(0, BATCH_SIZE);
|
|
49
|
+
try {
|
|
50
|
+
await Chat21SyncTrace.insertMany(batch, { ordered: false });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
winston.error("[Chat21SyncTracer] Error saving " + batch.length + " traces: ", err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} finally {
|
|
56
|
+
flushing = false;
|
|
57
|
+
}
|
|
15
58
|
}
|
|
16
59
|
|
|
17
60
|
function truncate(value) {
|
|
@@ -56,7 +99,7 @@ function describeRequest(request) {
|
|
|
56
99
|
}
|
|
57
100
|
|
|
58
101
|
/**
|
|
59
|
-
*
|
|
102
|
+
* Buffers a trace document, saved by the next flush. Never throws and never blocks the caller.
|
|
60
103
|
* @param {string} event the traced flow, e.g. 'request.close'
|
|
61
104
|
* @param {{request_id: string, id_project: *}} ref
|
|
62
105
|
* @param {string} step
|
|
@@ -67,32 +110,38 @@ function trace(event, ref, step, outcome, data) {
|
|
|
67
110
|
if (!enabled) {
|
|
68
111
|
return;
|
|
69
112
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
113
|
+
if (buffer.length >= MAX_BUFFER_SIZE) {
|
|
114
|
+
dropped++;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// createdAt is set here: the document is written later, and the timeline order must reflect the event time
|
|
118
|
+
buffer.push({
|
|
119
|
+
request_id: ref?.request_id,
|
|
120
|
+
id_project: ref?.id_project ? String(ref.id_project) : undefined,
|
|
121
|
+
event: event,
|
|
122
|
+
step: step,
|
|
123
|
+
outcome: outcome,
|
|
124
|
+
data: data,
|
|
125
|
+
host: host,
|
|
126
|
+
pid: process.pid,
|
|
127
|
+
createdAt: new Date()
|
|
128
|
+
});
|
|
129
|
+
if (buffer.length >= BATCH_SIZE) {
|
|
130
|
+
flush();
|
|
85
131
|
}
|
|
86
132
|
}
|
|
87
133
|
|
|
88
134
|
/**
|
|
89
135
|
* Traces a chat21 sdk call as <action>.sent, then <action>.response or <action>.error.
|
|
90
136
|
* The returned promise settles exactly like the one returned by callFn.
|
|
137
|
+
* @param {function} [outcomeOf] maps the response body to the outcome of <action>.response, default 'success'.
|
|
138
|
+
* The chat21 sdk resolves on any HTTP status code, so a resolved call is not necessarily successful.
|
|
91
139
|
*/
|
|
92
|
-
function traceCall(event, ref, action, data, callFn) {
|
|
140
|
+
function traceCall(event, ref, action, data, callFn, outcomeOf) {
|
|
93
141
|
trace(event, ref, action + '.sent', 'info', data);
|
|
94
142
|
return callFn().then((response) => {
|
|
95
|
-
|
|
143
|
+
const outcome = outcomeOf ? outcomeOf(response) : 'success';
|
|
144
|
+
trace(event, ref, action + '.response', outcome, { ...data, response: describeResponse(response) });
|
|
96
145
|
return response;
|
|
97
146
|
}, (err) => {
|
|
98
147
|
trace(event, ref, action + '.error', 'error', { ...data, error: describeError(err) });
|
|
@@ -104,6 +153,7 @@ module.exports = {
|
|
|
104
153
|
enabled,
|
|
105
154
|
trace,
|
|
106
155
|
traceCall,
|
|
156
|
+
flush,
|
|
107
157
|
describeError,
|
|
108
158
|
describeResponse,
|
|
109
159
|
describeRequest
|
|
@@ -271,6 +271,15 @@ class RequestService {
|
|
|
271
271
|
winston.debug("nobot:" + nobot);
|
|
272
272
|
//winston.info("main_flow_cache_3 route");
|
|
273
273
|
|
|
274
|
+
const traceRef = { request_id: request_id, id_project: id_project };
|
|
275
|
+
chat21SyncTracer.trace('request.participants.update', traceRef, 'service.called', 'info', {
|
|
276
|
+
method: 'route',
|
|
277
|
+
departmentid: departmentid,
|
|
278
|
+
nobot: nobot,
|
|
279
|
+
assignmentType: options?.assignmentType,
|
|
280
|
+
source: options?.source
|
|
281
|
+
});
|
|
282
|
+
|
|
274
283
|
// Find request
|
|
275
284
|
// Cache disabled: a stale cached document can have an outdated __v and cause
|
|
276
285
|
// Mongoose VersionError on save ("No matching document found for id ... version N").
|
|
@@ -304,7 +313,12 @@ class RequestService {
|
|
|
304
313
|
) {
|
|
305
314
|
|
|
306
315
|
winston.verbose(`Request ${request.request_id} routed to same status/participants`);
|
|
307
|
-
|
|
316
|
+
chat21SyncTracer.trace('request.participants.update', traceRef, 'service.unchanged', 'skipped', {
|
|
317
|
+
method: 'route',
|
|
318
|
+
...chat21SyncTracer.describeRequest(request)
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
let abandonedStatusSaved = false;
|
|
308
322
|
if (routedRequest.attributes?.fully_abandoned === true) {
|
|
309
323
|
request.status = RequestConstants.ABANDONED;
|
|
310
324
|
request.attributes.fully_abandoned = true;
|
|
@@ -313,6 +327,7 @@ class RequestService {
|
|
|
313
327
|
|
|
314
328
|
try {
|
|
315
329
|
await request.save();
|
|
330
|
+
abandonedStatusSaved = true;
|
|
316
331
|
winston.verbose(`Status set to ABANDONED for request ${request._id}`);
|
|
317
332
|
} catch (err) {
|
|
318
333
|
winston.error("Error updating request to ABANDONED", err);
|
|
@@ -337,18 +352,13 @@ class RequestService {
|
|
|
337
352
|
.populate("participatingAgents")
|
|
338
353
|
.populate({ path: "requester", populate: { path: "id_user" } })
|
|
339
354
|
.execPopulate();
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
previousDepartmentId: beforeDepartmentId,
|
|
348
|
-
departmentId: afterDepartmentId,
|
|
349
|
-
departmentName: requestComplete.department && requestComplete.department.name
|
|
350
|
-
} : undefined
|
|
351
|
-
);
|
|
355
|
+
|
|
356
|
+
// No participants events here: queues call route every few seconds on unassigned requests and each
|
|
357
|
+
// request.participants.update resends the chat21 members, the agent emails and the welcome message
|
|
358
|
+
if (abandonedStatusSaved) {
|
|
359
|
+
requestEvent.emit("request.update", requestComplete);
|
|
360
|
+
requestEvent.emit("request.updated", { comment: "STATUS_CHANGE", request: requestComplete, patch: { status: requestComplete.status } });
|
|
361
|
+
}
|
|
352
362
|
|
|
353
363
|
return requestComplete;
|
|
354
364
|
}
|
|
@@ -2627,6 +2637,7 @@ class RequestService {
|
|
|
2627
2637
|
patch: { removedParticipants, addedParticipants }
|
|
2628
2638
|
});
|
|
2629
2639
|
|
|
2640
|
+
traceEventEmitted('request.participants.update', requestComplete, requestComplete, { removedParticipants, addedParticipants });
|
|
2630
2641
|
requestEvent.emit("request.participants.update", {
|
|
2631
2642
|
beforeRequest,
|
|
2632
2643
|
removedParticipants,
|