Skip to content

Commit 143fa7a

Browse files
authored
perf: speed up member list function (#867)
Previously, `MemberApi.prototype.getMany` would iterate through `allDeviceInfo` for every role. Now, it only iterates through `allDeviceInfo` once.
1 parent 267fd69 commit 143fa7a

File tree

3 files changed

+65
-3
lines changed

3 files changed

+65
-3
lines changed

src/lib/key-by.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Like [`Map.groupBy`][0], but the result's values aren't arrays.
3+
*
4+
* If multiple values resolve to the same key, an error is thrown.
5+
*
6+
* [0]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/groupBy
7+
*
8+
* @template T
9+
* @template K
10+
* @param {Iterable<T>} items
11+
* @param {(item: T) => K} callbackFn
12+
* @returns {Map<K, T>}
13+
*/
14+
export function keyBy(items, callbackFn) {
15+
/** @type {Map<K, T>} */ const result = new Map()
16+
for (const item of items) {
17+
const key = callbackFn(item)
18+
if (result.has(key)) {
19+
throw new Error(`keyBy found duplicate key ${JSON.stringify(key)}`)
20+
}
21+
result.set(key, item)
22+
}
23+
return result
24+
}

src/member-api.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
projectKeyToId,
1010
projectKeyToProjectInviteId,
1111
} from './utils.js'
12+
import { keyBy } from './lib/key-by.js'
1213
import { abortSignalAny } from './lib/ponyfills.js'
1314
import timingSafeEqual from './lib/timing-safe-equal.js'
1415
import { ROLES, isRoleIdForNewInvite } from './roles.js'
@@ -279,6 +280,8 @@ export class MemberApi extends TypedEmitter {
279280
this.#dataTypes.deviceInfo.getMany(),
280281
])
281282

283+
const deviceInfoByConfigCoreId = keyBy(allDeviceInfo, ({ docId }) => docId)
284+
282285
return Promise.all(
283286
[...allRoles.entries()].map(async ([deviceId, role]) => {
284287
/** @type {MemberInfo} */
@@ -290,9 +293,7 @@ export class MemberApi extends TypedEmitter {
290293
'config'
291294
)
292295

293-
const deviceInfo = allDeviceInfo.find(
294-
({ docId }) => docId === configCoreId
295-
)
296+
const deviceInfo = deviceInfoByConfigCoreId.get(configCoreId)
296297

297298
memberInfo.name = deviceInfo?.name
298299
memberInfo.deviceType = deviceInfo?.deviceType

tests/lib/key-by.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import assert from 'node:assert/strict'
2+
import test from 'node:test'
3+
import { keyBy } from '../../src/lib/key-by.js'
4+
5+
test('returns an empty map if passed an empty iterable', () => {
6+
assert.deepEqual(
7+
keyBy([], () => {
8+
throw new Error('Should not be called')
9+
}),
10+
new Map()
11+
)
12+
})
13+
14+
test('keys a list of items by a key function', () => {
15+
const items = [
16+
{ id: 1, name: 'foo' },
17+
{ id: 2, name: 'bar' },
18+
{ id: 3, name: 'baz' },
19+
]
20+
const result = keyBy(items, (item) => item.id)
21+
assert.deepEqual(
22+
result,
23+
new Map([
24+
[1, items[0]],
25+
[2, items[1]],
26+
[3, items[2]],
27+
])
28+
)
29+
})
30+
31+
test('duplicate keys', () => {
32+
const items = [
33+
{ id: 1, name: 'foo' },
34+
{ id: 1, name: 'bar' },
35+
]
36+
assert.throws(() => keyBy(items, (item) => item.id))
37+
})

0 commit comments

Comments
 (0)