Skip to content

Commit 7314c49

Browse files
authored
Merge pull request #4361 from reduxjs/feature/4252-entity-adapter-sorting
2 parents 00d46d9 + 0ca3c60 commit 7314c49

File tree

3 files changed

+333
-33
lines changed

3 files changed

+333
-33
lines changed

packages/toolkit/src/entities/sorted_state_adapter.ts

+114-21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { current, isDraft } from 'immer'
12
import type {
23
IdSelector,
34
Comparer,
@@ -12,11 +13,46 @@ import {
1213
selectIdValue,
1314
ensureEntitiesArray,
1415
splitAddedUpdatedEntities,
16+
getCurrent,
1517
} from './utils'
1618

19+
// Borrowed from Replay
20+
export function findInsertIndex<T>(
21+
sortedItems: T[],
22+
item: T,
23+
comparisonFunction: Comparer<T>,
24+
): number {
25+
let lowIndex = 0
26+
let highIndex = sortedItems.length
27+
while (lowIndex < highIndex) {
28+
let middleIndex = (lowIndex + highIndex) >>> 1
29+
const currentItem = sortedItems[middleIndex]
30+
const res = comparisonFunction(item, currentItem)
31+
if (res >= 0) {
32+
lowIndex = middleIndex + 1
33+
} else {
34+
highIndex = middleIndex
35+
}
36+
}
37+
38+
return lowIndex
39+
}
40+
41+
export function insert<T>(
42+
sortedItems: T[],
43+
item: T,
44+
comparisonFunction: Comparer<T>,
45+
): T[] {
46+
const insertAtIndex = findInsertIndex(sortedItems, item, comparisonFunction)
47+
48+
sortedItems.splice(insertAtIndex, 0, item)
49+
50+
return sortedItems
51+
}
52+
1753
export function createSortedStateAdapter<T, Id extends EntityId>(
1854
selectId: IdSelector<T, Id>,
19-
sort: Comparer<T>,
55+
comparer: Comparer<T>,
2056
): EntityStateAdapter<T, Id> {
2157
type R = DraftableEntityState<T, Id>
2258

@@ -30,15 +66,20 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
3066
function addManyMutably(
3167
newEntities: readonly T[] | Record<Id, T>,
3268
state: R,
69+
existingIds?: Id[],
3370
): void {
3471
newEntities = ensureEntitiesArray(newEntities)
3572

73+
const existingKeys = new Set<Id>(
74+
existingIds ?? (current(state.ids) as Id[]),
75+
)
76+
3677
const models = newEntities.filter(
37-
(model) => !(selectIdValue(model, selectId) in state.entities),
78+
(model) => !existingKeys.has(selectIdValue(model, selectId)),
3879
)
3980

4081
if (models.length !== 0) {
41-
merge(models, state)
82+
mergeFunction(state, models)
4283
}
4384
}
4485

@@ -52,7 +93,10 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
5293
): void {
5394
newEntities = ensureEntitiesArray(newEntities)
5495
if (newEntities.length !== 0) {
55-
merge(newEntities, state)
96+
for (const item of newEntities) {
97+
delete (state.entities as Record<Id, T>)[selectId(item)]
98+
}
99+
mergeFunction(state, newEntities)
56100
}
57101
}
58102

@@ -64,7 +108,7 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
64108
state.entities = {} as Record<Id, T>
65109
state.ids = []
66110

67-
addManyMutably(newEntities, state)
111+
addManyMutably(newEntities, state, [])
68112
}
69113

70114
function updateOneMutably(update: Update<T, Id>, state: R): void {
@@ -76,6 +120,7 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
76120
state: R,
77121
): void {
78122
let appliedUpdates = false
123+
let replacedIds = false
79124

80125
for (let update of updates) {
81126
const entity: T | undefined = (state.entities as Record<Id, T>)[update.id]
@@ -87,14 +132,20 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
87132

88133
Object.assign(entity, update.changes)
89134
const newId = selectId(entity)
135+
90136
if (update.id !== newId) {
137+
// We do support the case where updates can change an item's ID.
138+
// This makes things trickier - go ahead and swap the IDs in state now.
139+
replacedIds = true
91140
delete (state.entities as Record<Id, T>)[update.id]
141+
const oldIndex = (state.ids as Id[]).indexOf(update.id)
142+
state.ids[oldIndex] = newId
92143
;(state.entities as Record<Id, T>)[newId] = entity
93144
}
94145
}
95146

96147
if (appliedUpdates) {
97-
resortEntities(state)
148+
mergeFunction(state, [], appliedUpdates, replacedIds)
98149
}
99150
}
100151

@@ -106,14 +157,18 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
106157
newEntities: readonly T[] | Record<Id, T>,
107158
state: R,
108159
): void {
109-
const [added, updated] = splitAddedUpdatedEntities<T, Id>(
160+
const [added, updated, existingIdsArray] = splitAddedUpdatedEntities<T, Id>(
110161
newEntities,
111162
selectId,
112163
state,
113164
)
114165

115-
updateManyMutably(updated, state)
116-
addManyMutably(added, state)
166+
if (updated.length) {
167+
updateManyMutably(updated, state)
168+
}
169+
if (added.length) {
170+
addManyMutably(added, state, existingIdsArray)
171+
}
117172
}
118173

119174
function areArraysEqual(a: readonly unknown[], b: readonly unknown[]) {
@@ -130,27 +185,65 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
130185
return true
131186
}
132187

133-
function merge(models: readonly T[], state: R): void {
188+
type MergeFunction = (
189+
state: R,
190+
addedItems: readonly T[],
191+
appliedUpdates?: boolean,
192+
replacedIds?: boolean,
193+
) => void
194+
195+
const mergeInsertion: MergeFunction = (
196+
state,
197+
addedItems,
198+
appliedUpdates,
199+
replacedIds,
200+
) => {
201+
const currentEntities = getCurrent(state.entities) as Record<Id, T>
202+
const currentIds = getCurrent(state.ids) as Id[]
203+
204+
const stateEntities = state.entities as Record<Id, T>
205+
206+
let ids = currentIds
207+
if (replacedIds) {
208+
ids = Array.from(new Set(currentIds))
209+
}
210+
211+
let sortedEntities: T[] = []
212+
for (const id of ids) {
213+
const entity = currentEntities[id]
214+
if (entity) {
215+
sortedEntities.push(entity)
216+
}
217+
}
218+
const wasPreviouslyEmpty = sortedEntities.length === 0
219+
134220
// Insert/overwrite all new/updated
135-
models.forEach((model) => {
136-
;(state.entities as Record<Id, T>)[selectId(model)] = model
137-
})
221+
for (const item of addedItems) {
222+
stateEntities[selectId(item)] = item
138223

139-
resortEntities(state)
140-
}
224+
if (!wasPreviouslyEmpty) {
225+
// Binary search insertion generally requires fewer comparisons
226+
insert(sortedEntities, item, comparer)
227+
}
228+
}
141229

142-
function resortEntities(state: R) {
143-
const allEntities = Object.values(state.entities) as T[]
144-
allEntities.sort(sort)
230+
if (wasPreviouslyEmpty) {
231+
// All we have is the incoming values, sort them
232+
sortedEntities = addedItems.slice().sort(comparer)
233+
} else if (appliedUpdates) {
234+
// We should have a _mostly_-sorted array already
235+
sortedEntities.sort(comparer)
236+
}
145237

146-
const newSortedIds = allEntities.map(selectId)
147-
const { ids } = state
238+
const newSortedIds = sortedEntities.map(selectId)
148239

149-
if (!areArraysEqual(ids, newSortedIds)) {
240+
if (!areArraysEqual(currentIds, newSortedIds)) {
150241
state.ids = newSortedIds
151242
}
152243
}
153244

245+
const mergeFunction: MergeFunction = mergeInsertion
246+
154247
return {
155248
removeOne,
156249
removeMany,

0 commit comments

Comments
 (0)