Skip to content

Commit 076aea4

Browse files
committed
feat(perf): rm duplicated ids from bathched queries
1 parent fd26eca commit 076aea4

File tree

4 files changed

+49
-58
lines changed

4 files changed

+49
-58
lines changed

src/dataLoaders.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,15 @@ export default class DataLoaders {
8484
T extends { [key: string]: any },
8585
FieldType extends object | string
8686
>(collectionName: string, values: ReadonlyArray<FieldType>, fieldName: string): Promise<(T | null | Error)[]> {
87+
const valuesMap = new Map<string, FieldType>();
88+
89+
for (const value of values) {
90+
valuesMap.set(value.toString(), value);
91+
}
92+
8793
const queryResult = await this.dbConnection.collection(collectionName)
8894
.find({
89-
[fieldName]: { $in: values },
95+
[fieldName]: { $in: Array.from(valuesMap.values()) },
9096
})
9197
.toArray();
9298

@@ -115,7 +121,10 @@ export function createProjectEventsByIdLoader(
115121
projectId: string
116122
): DataLoader<string, EventDbScheme | null> {
117123
return new DataLoader<string, EventDbScheme | null>(async (ids) => {
118-
const objectIds = ids.map((id) => new ObjectId(id));
124+
/**
125+
* Deduplicate only for the DB query; keep original ids array for mapping
126+
*/
127+
const objectIds = [ ...new Set(ids) ].map(id => new ObjectId(id));
119128

120129
const docs = await eventsDb
121130
.collection(`events:${projectId}`)

src/resolvers/event.js

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,6 @@
1-
const EventsFactory = require('../models/eventsFactory');
1+
const getEventsFactory = require('./helpers/eventsFactory').default;
22
const sendPersonalNotification = require('../utils/personalNotifications').default;
33

4-
/**
5-
* Returns a per-request, per-project EventsFactory instance
6-
* Uses context.eventsFactoryCache to memoize by projectId
7-
*
8-
* @param {ResolverContextBase} context - resolver context
9-
* @param {string} projectId - project id to get EventsFactory instance for
10-
* @returns {EventsFactory} - EventsFactory instance bound to a specific project object
11-
*/
12-
function getEventsFactoryForProjectId(context, projectId) {
13-
const cache = context.eventsFactoryCache || (context.eventsFactoryCache = new Map());
14-
const cacheKey = projectId.toString();
15-
16-
if (!cache.has(cacheKey)) {
17-
cache.set(cacheKey, new EventsFactory(projectId));
18-
}
19-
20-
return cache.get(cacheKey);
21-
}
22-
234
/**
245
* See all types and fields here {@see ../typeDefs/event.graphql}
256
*/
@@ -48,7 +29,7 @@ module.exports = {
4829
* @return {RepetitionsPortion}
4930
*/
5031
async repetitionsPortion({ projectId, originalEventId }, { limit, cursor }, context) {
51-
const factory = getEventsFactoryForProjectId(context, projectId);
32+
const factory = getEventsFactory(context, projectId);
5233

5334
return factory.getEventRepetitions(originalEventId, limit, cursor);
5435
},
@@ -103,7 +84,7 @@ module.exports = {
10384
* @returns {Promise<ProjectChartItem[]>}
10485
*/
10586
async chartData({ projectId, groupHash }, { days, timezoneOffset }, context) {
106-
const factory = getEventsFactoryForProjectId(context, projectId);
87+
const factory = getEventsFactory(context, projectId);
10788

10889
return factory.findChartData(days, timezoneOffset, groupHash);
10990
},
@@ -116,7 +97,7 @@ module.exports = {
11697
* @returns {Promise<Release>}
11798
*/
11899
async release({ projectId, id: eventId }, _args, context) {
119-
const factory = getEventsFactoryForProjectId(context, projectId);
100+
const factory = getEventsFactory(context, projectId);
120101
const release = await factory.getEventRelease(eventId);
121102

122103
return release;
@@ -133,7 +114,7 @@ module.exports = {
133114
* @return {Promise<boolean>}
134115
*/
135116
async visitEvent(_obj, { projectId, eventId }, { user, ...context }) {
136-
const factory = getEventsFactoryForProjectId(context, projectId);
117+
const factory = getEventsFactory(context, projectId);
137118

138119
const { result } = await factory.visitEvent(eventId, user.id);
139120

@@ -150,7 +131,7 @@ module.exports = {
150131
* @return {Promise<boolean>}
151132
*/
152133
async toggleEventMark(_obj, { project, eventId, mark }, context) {
153-
const factory = getEventsFactoryForProjectId(context, project);
134+
const factory = getEventsFactory(context, project);
154135

155136
const { result } = await factory.toggleEventMark(eventId, mark);
156137

@@ -175,7 +156,7 @@ module.exports = {
175156
*/
176157
async updateAssignee(_obj, { input }, { factories, user, ...context }) {
177158
const { projectId, eventId, assignee } = input;
178-
const factory = getEventsFactoryForProjectId(context, projectId);
159+
const factory = getEventsFactory(context, projectId);
179160

180161
const userExists = await factories.usersFactory.findById(assignee);
181162

@@ -226,7 +207,7 @@ module.exports = {
226207
*/
227208
async removeAssignee(_obj, { input }, context) {
228209
const { projectId, eventId } = input;
229-
const factory = getEventsFactoryForProjectId(context, projectId);
210+
const factory = getEventsFactory(context, projectId);
230211

231212
const { result } = await factory.updateAssignee(eventId, '');
232213

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ResolverContextBase } from '../../types/graphql';
2+
3+
// eslint-disable-next-line @typescript-eslint/no-var-requires
4+
const EventsFactory = require('../../models/eventsFactory');
5+
6+
/**
7+
* Returns a request-scoped, per-project EventsFactory instance using context cache
8+
* Falls back to a fresh instance if cache is not available (shouldn't happen in normal flow)
9+
*/
10+
export function getEventsFactory(context: ResolverContextBase, projectId: string) {
11+
const cache = context && context.eventsFactoryCache;
12+
13+
if (cache) {
14+
if (!cache.has(projectId)) {
15+
cache.set(projectId, new EventsFactory(projectId));
16+
}
17+
18+
return cache.get(projectId);
19+
}
20+
21+
return new EventsFactory(projectId);
22+
}
23+
24+
export default getEventsFactory;

src/resolvers/project.js

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { ApolloError, UserInputError } = require('apollo-server-express');
55
const Validator = require('../utils/validator');
66
const UserInProject = require('../models/userInProject');
77
const EventsFactory = require('../models/eventsFactory');
8+
const getEventsFactory = require('./helpers/eventsFactory').default;
89
const ProjectToWorkspace = require('../models/projectToWorkspace');
910
const { dateFromObjectId } = require('../utils/dates');
1011
const ProjectModel = require('../models/project').default;
@@ -14,30 +15,6 @@ const REPETITIONS_GROUP_HASH_INDEX_NAME = 'groupHash_hashed';
1415
const REPETITIONS_USER_ID_INDEX_NAME = 'userId';
1516
const MAX_SEARCH_QUERY_LENGTH = 50;
1617

17-
/**
18-
* Returns a singleton EventsFactory instance bound to a specific project object.
19-
* Uses request-scoped cache to share across nested resolvers.
20-
*
21-
* @param {ProjectDBScheme|Object} project - project instance to make a instance of EventsFactory
22-
* @param {ResolverContextBase} context - resolver context
23-
* @returns {EventsFactory} - EventsFactory instance bound to a specific project object
24-
*/
25-
function getEventsFactoryForProject(project, context) {
26-
const cache = context && context.eventsFactoryCache;
27-
const key = project._id.toString();
28-
29-
if (cache) {
30-
if (!cache.has(key)) {
31-
cache.set(key, new EventsFactory(project._id));
32-
}
33-
34-
return cache.get(key);
35-
}
36-
37-
// Fallback (shouldn't happen in normal resolver flow): return a fresh instance
38-
return new EventsFactory(project._id);
39-
}
40-
4118
/**
4219
* See all types and fields here {@see ../typeDefs/project.graphql}
4320
*/
@@ -313,7 +290,7 @@ module.exports = {
313290
* @returns {EventRepetitionSchema}
314291
*/
315292
async event(project, { eventId: repetitionId, originalEventId }, context) {
316-
const factory = getEventsFactoryForProject(project, context);
293+
const factory = getEventsFactory(context, project._id);
317294
const repetition = await factory.getEventRepetition(repetitionId, originalEventId);
318295

319296
if (!repetition) {
@@ -335,7 +312,7 @@ module.exports = {
335312
* @returns {Event[]}
336313
*/
337314
async events(project, { limit, skip }, context) {
338-
const factory = getEventsFactoryForProject(project, context);
315+
const factory = getEventsFactory(context, project._id);
339316

340317
return factory.find({}, limit, skip);
341318
},
@@ -350,7 +327,7 @@ module.exports = {
350327
* @return {Promise<number>}
351328
*/
352329
async unreadCount(project, data, { user, ...context }) {
353-
const eventsFactory = getEventsFactoryForProject(project, context);
330+
const eventsFactory = getEventsFactory(context, project._id);
354331
const userInProject = new UserInProject(user.id, project._id);
355332
const lastVisit = await userInProject.getLastVisit();
356333

@@ -376,7 +353,7 @@ module.exports = {
376353
}
377354
}
378355

379-
const factory = getEventsFactoryForProject(project, context);
356+
const factory = getEventsFactory(context, project._id);
380357

381358
const dailyEventsPortion = await factory.findDailyEventsPortion(limit, nextCursor, sort, filters, search);
382359

@@ -393,7 +370,7 @@ module.exports = {
393370
* @return {Promise<ProjectChartItem[]>}
394371
*/
395372
async chartData(project, { days, timezoneOffset }, context) {
396-
const factory = getEventsFactoryForProject(project, context);
373+
const factory = getEventsFactory(context, project._id);
397374

398375
return factory.findChartData(days, timezoneOffset);
399376
},

0 commit comments

Comments
 (0)