|
1 | 1 | import type { EntityAdapter, EntityState } from '../models'
|
2 | 2 | import { createEntityAdapter } from '../create_adapter'
|
3 |
| -import { createAction, createSlice, configureStore } from '@reduxjs/toolkit' |
| 3 | +import { |
| 4 | + createAction, |
| 5 | + createSlice, |
| 6 | + configureStore, |
| 7 | + nanoid, |
| 8 | +} from '@reduxjs/toolkit' |
4 | 9 | import type { BookModel } from './fixtures/book'
|
5 | 10 | import {
|
6 | 11 | TheGreatGatsby,
|
@@ -247,7 +252,7 @@ describe('Sorted State Adapter', () => {
|
247 | 252 |
|
248 | 253 | const { ids, entities } = withUpdated
|
249 | 254 |
|
250 |
| - expect(ids.length).toBe(2) |
| 255 | + expect(ids).toEqual(['a', 'c']) |
251 | 256 | expect(entities.a).toBeTruthy()
|
252 | 257 | expect(entities.b).not.toBeTruthy()
|
253 | 258 | expect(entities.c).toBeTruthy()
|
@@ -584,6 +589,86 @@ describe('Sorted State Adapter', () => {
|
584 | 589 | expect(withUpdate.entities['b']!.title).toBe(book1.title)
|
585 | 590 | })
|
586 | 591 |
|
| 592 | + it('should minimize the amount of sorting work needed', () => { |
| 593 | + const PARAMETERS = { |
| 594 | + NUM_ITEMS: 10_000, |
| 595 | + } |
| 596 | + |
| 597 | + type Entity = { id: string; name: string; position: number } |
| 598 | + |
| 599 | + let numSorts = 0 |
| 600 | + |
| 601 | + const adaptor = createEntityAdapter({ |
| 602 | + selectId: (entity: Entity) => entity.id, |
| 603 | + sortComparer: (a, b) => { |
| 604 | + numSorts++ |
| 605 | + if (a.position < b.position) return -1 |
| 606 | + else if (a.position > b.position) return 1 |
| 607 | + return 0 |
| 608 | + }, |
| 609 | + }) |
| 610 | + |
| 611 | + const initialState: Entity[] = new Array(PARAMETERS.NUM_ITEMS) |
| 612 | + .fill(undefined) |
| 613 | + .map((x, i) => ({ |
| 614 | + name: `${i}`, |
| 615 | + position: Math.random(), |
| 616 | + id: nanoid(), |
| 617 | + })) |
| 618 | + |
| 619 | + const entitySlice = createSlice({ |
| 620 | + name: 'entity', |
| 621 | + initialState: adaptor.getInitialState(undefined, initialState), |
| 622 | + reducers: { |
| 623 | + updateOne: adaptor.updateOne, |
| 624 | + upsertOne: adaptor.upsertOne, |
| 625 | + upsertMany: adaptor.upsertMany, |
| 626 | + }, |
| 627 | + }) |
| 628 | + |
| 629 | + const store = configureStore({ |
| 630 | + reducer: { |
| 631 | + entity: entitySlice.reducer, |
| 632 | + }, |
| 633 | + middleware: (getDefaultMiddleware) => { |
| 634 | + return getDefaultMiddleware({ |
| 635 | + serializableCheck: false, |
| 636 | + immutableCheck: false, |
| 637 | + }) |
| 638 | + }, |
| 639 | + }) |
| 640 | + |
| 641 | + store.dispatch( |
| 642 | + entitySlice.actions.upsertOne({ |
| 643 | + id: nanoid(), |
| 644 | + position: Math.random(), |
| 645 | + name: 'test', |
| 646 | + }), |
| 647 | + ) |
| 648 | + |
| 649 | + // These numbers will vary because of the randomness, but generally |
| 650 | + // with 10K items the old code had 200K+ sort calls, while the new code |
| 651 | + // is around 130K sort calls. |
| 652 | + expect(numSorts).toBeLessThan(200_000) |
| 653 | + |
| 654 | + const { ids } = store.getState().entity |
| 655 | + const middleItemId = ids[(ids.length / 2) | 0] |
| 656 | + |
| 657 | + numSorts = 0 |
| 658 | + |
| 659 | + store.dispatch( |
| 660 | + // Move this middle item near the end |
| 661 | + entitySlice.actions.updateOne({ |
| 662 | + id: middleItemId, |
| 663 | + changes: { |
| 664 | + position: 0.99999, |
| 665 | + }, |
| 666 | + }), |
| 667 | + ) |
| 668 | + // The old code was around 120K, the new code is around 10K. |
| 669 | + expect(numSorts).toBeLessThan(25_000) |
| 670 | + }) |
| 671 | + |
587 | 672 | describe('can be used mutably when wrapped in createNextState', () => {
|
588 | 673 | test('removeAll', () => {
|
589 | 674 | const withTwo = adapter.addMany(state, [TheGreatGatsby, AnimalFarm])
|
|
0 commit comments