1
+ import { current , isDraft } from 'immer'
1
2
import type {
2
3
IdSelector ,
3
4
Comparer ,
@@ -12,11 +13,46 @@ import {
12
13
selectIdValue ,
13
14
ensureEntitiesArray ,
14
15
splitAddedUpdatedEntities ,
16
+ getCurrent ,
15
17
} from './utils'
16
18
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
+
17
53
export function createSortedStateAdapter < T , Id extends EntityId > (
18
54
selectId : IdSelector < T , Id > ,
19
- sort : Comparer < T > ,
55
+ comparer : Comparer < T > ,
20
56
) : EntityStateAdapter < T , Id > {
21
57
type R = DraftableEntityState < T , Id >
22
58
@@ -30,15 +66,20 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
30
66
function addManyMutably (
31
67
newEntities : readonly T [ ] | Record < Id , T > ,
32
68
state : R ,
69
+ existingIds ?: Id [ ] ,
33
70
) : void {
34
71
newEntities = ensureEntitiesArray ( newEntities )
35
72
73
+ const existingKeys = new Set < Id > (
74
+ existingIds ?? ( current ( state . ids ) as Id [ ] ) ,
75
+ )
76
+
36
77
const models = newEntities . filter (
37
- ( model ) => ! ( selectIdValue ( model , selectId ) in state . entities ) ,
78
+ ( model ) => ! existingKeys . has ( selectIdValue ( model , selectId ) ) ,
38
79
)
39
80
40
81
if ( models . length !== 0 ) {
41
- merge ( models , state )
82
+ mergeFunction ( state , models )
42
83
}
43
84
}
44
85
@@ -52,7 +93,10 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
52
93
) : void {
53
94
newEntities = ensureEntitiesArray ( newEntities )
54
95
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 )
56
100
}
57
101
}
58
102
@@ -64,7 +108,7 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
64
108
state . entities = { } as Record < Id , T >
65
109
state . ids = [ ]
66
110
67
- addManyMutably ( newEntities , state )
111
+ addManyMutably ( newEntities , state , [ ] )
68
112
}
69
113
70
114
function updateOneMutably ( update : Update < T , Id > , state : R ) : void {
@@ -76,6 +120,7 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
76
120
state : R ,
77
121
) : void {
78
122
let appliedUpdates = false
123
+ let replacedIds = false
79
124
80
125
for ( let update of updates ) {
81
126
const entity : T | undefined = ( state . entities as Record < Id , T > ) [ update . id ]
@@ -87,14 +132,20 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
87
132
88
133
Object . assign ( entity , update . changes )
89
134
const newId = selectId ( entity )
135
+
90
136
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
91
140
delete ( state . entities as Record < Id , T > ) [ update . id ]
141
+ const oldIndex = ( state . ids as Id [ ] ) . indexOf ( update . id )
142
+ state . ids [ oldIndex ] = newId
92
143
; ( state . entities as Record < Id , T > ) [ newId ] = entity
93
144
}
94
145
}
95
146
96
147
if ( appliedUpdates ) {
97
- resortEntities ( state )
148
+ mergeFunction ( state , [ ] , appliedUpdates , replacedIds )
98
149
}
99
150
}
100
151
@@ -106,14 +157,18 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
106
157
newEntities : readonly T [ ] | Record < Id , T > ,
107
158
state : R ,
108
159
) : void {
109
- const [ added , updated ] = splitAddedUpdatedEntities < T , Id > (
160
+ const [ added , updated , existingIdsArray ] = splitAddedUpdatedEntities < T , Id > (
110
161
newEntities ,
111
162
selectId ,
112
163
state ,
113
164
)
114
165
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
+ }
117
172
}
118
173
119
174
function areArraysEqual ( a : readonly unknown [ ] , b : readonly unknown [ ] ) {
@@ -130,27 +185,65 @@ export function createSortedStateAdapter<T, Id extends EntityId>(
130
185
return true
131
186
}
132
187
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
+
134
220
// 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
138
223
139
- resortEntities ( state )
140
- }
224
+ if ( ! wasPreviouslyEmpty ) {
225
+ // Binary search insertion generally requires fewer comparisons
226
+ insert ( sortedEntities , item , comparer )
227
+ }
228
+ }
141
229
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
+ }
145
237
146
- const newSortedIds = allEntities . map ( selectId )
147
- const { ids } = state
238
+ const newSortedIds = sortedEntities . map ( selectId )
148
239
149
- if ( ! areArraysEqual ( ids , newSortedIds ) ) {
240
+ if ( ! areArraysEqual ( currentIds , newSortedIds ) ) {
150
241
state . ids = newSortedIds
151
242
}
152
243
}
153
244
245
+ const mergeFunction : MergeFunction = mergeInsertion
246
+
154
247
return {
155
248
removeOne,
156
249
removeMany,
0 commit comments