Skip to content

Commit

Permalink
perf: speed up member list function
Browse files Browse the repository at this point in the history
Previously, `MemberApi.prototype.getMany` would iterate through
`allDeviceInfo` for every role.

Now, it only iterates through `allDeviceInfo` once.
  • Loading branch information
EvanHahn committed Sep 25, 2024
1 parent e8764c4 commit b27270e
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 3 deletions.
24 changes: 24 additions & 0 deletions src/lib/key-by.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Like [`Map.groupBy`][0], but the result's values aren't arrays.
*
* If multiple values resolve to the same key, an error is thrown.
*
* [0]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/groupBy
*
* @template T
* @template K
* @param {Iterable<T>} items
* @param {(item: T) => K} callbackFn
* @returns {Map<K, T>}
*/
export function keyBy(items, callbackFn) {
/** @type {Map<K, T>} */ const result = new Map()
for (const item of items) {
const key = callbackFn(item)
if (result.has(key)) {
throw new Error(`keyBy found duplicate key ${JSON.stringify(key)}`)
}
result.set(key, item)
}
return result
}
7 changes: 4 additions & 3 deletions src/member-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
projectKeyToId,
projectKeyToProjectInviteId,
} from './utils.js'
import { keyBy } from './lib/key-by.js'
import { abortSignalAny } from './lib/ponyfills.js'
import timingSafeEqual from './lib/timing-safe-equal.js'
import { ROLES, isRoleIdForNewInvite } from './roles.js'
Expand Down Expand Up @@ -279,6 +280,8 @@ export class MemberApi extends TypedEmitter {
this.#dataTypes.deviceInfo.getMany(),
])

const deviceInfoByConfigCoreId = keyBy(allDeviceInfo, ({ docId }) => docId)

return Promise.all(
[...allRoles.entries()].map(async ([deviceId, role]) => {
/** @type {MemberInfo} */
Expand All @@ -290,9 +293,7 @@ export class MemberApi extends TypedEmitter {
'config'
)

const deviceInfo = allDeviceInfo.find(
({ docId }) => docId === configCoreId
)
const deviceInfo = deviceInfoByConfigCoreId.get(configCoreId)

memberInfo.name = deviceInfo?.name
memberInfo.deviceType = deviceInfo?.deviceType
Expand Down
37 changes: 37 additions & 0 deletions tests/lib/key-by.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { keyBy } from '../../src/lib/key-by.js'

test('returns an empty map if passed an empty iterable', () => {
assert.deepEqual(
keyBy([], () => {
throw new Error('Should not be called')
}),
new Map()
)
})

test('keys a list of items by a key function', () => {
const items = [
{ id: 1, name: 'foo' },
{ id: 2, name: 'bar' },
{ id: 3, name: 'baz' },
]
const result = keyBy(items, (item) => item.id)
assert.deepEqual(
result,
new Map([
[1, items[0]],
[2, items[1]],
[3, items[2]],
])
)
})

test('duplicate keys', () => {
const items = [
{ id: 1, name: 'foo' },
{ id: 1, name: 'bar' },
]
assert.throws(() => keyBy(items, (item) => item.id))
})

0 comments on commit b27270e

Please sign in to comment.