Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/query/.size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
"gzip": true,
"path": "dist/index.production.js",
"import": "*",
"limit": "4.6 kB"
"limit": "4.75 kB"
},
{
"name": "All publics (Brotli)",
"path": "dist/index.production.js",
"import": "*",
"limit": "4.25 kB"
"limit": "4.35 kB"
},
{
"name": "Minimal set (Gzip)",
Expand Down
71 changes: 71 additions & 0 deletions packages/query/src/cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ import {
dataCacheFacade,
errorCacheFacade
} from './cache.js'
import {
append,
drop,
mapFirstPage,
mapPages,
restore,
restorePages
} from './utils.js'
import type { InfinitePages } from './client.js'

describe('query', () => {
describe('cache', () => {
Expand Down Expand Up @@ -402,6 +411,68 @@ describe('query', () => {
expect($cache(CountKey('a'))).toBe(1)
expect($cache(CountKey('b'))).toBe(2)
})

it('should take a removal back with `restore`', () => {
const ListKey = queryKey<[], number[]>('list')
const $cache = dataCacheFacade(new CacheStorage())
const key = ListKey()
const isTwo = (value: number) => value === 2

$cache(key, [1, 2, 3])

const revert = $cache(
key,
list => drop(list, isTwo),
(current, prev) => restore(current, prev, isTwo)
)

$cache(key, list => append(list, 4))
revert()

expect($cache(key)).toEqual([1, 2, 3, 4])
})

it('should take a removal from pages back with `restorePages`', () => {
interface Page {
items: number[]
}

const PagesKey = queryKey<[], InfinitePages<Page, number>>('pages')
const $cache = dataCacheFacade(new CacheStorage())
const key = PagesKey()
const isTwo = (value: number) => value === 2

$cache(key, {
pages: [
{
items: [1, 2]
}
],
next: 2,
more: true
})

const revert = $cache(
key,
pages => mapPages(pages, page => ({
items: drop(page.items, isTwo)
})),
(current, prev) => restorePages(current, prev, isTwo, (restoreItems, page) => ({
items: restoreItems(page.items)
}))
)

$cache(key, pages => mapFirstPage(pages, page => ({
items: append(page.items, 3)
})))
revert()

expect($cache(key)?.pages).toEqual([
{
items: [1, 2, 3]
}
])
})
})
})

Expand Down
180 changes: 179 additions & 1 deletion packages/query/src/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import {
append,
prepend,
drop,
restore,
map,
sort,
mapPages,
mapPageAt,
mapFirstPage,
mapLastPage,
sortPages
sortPages,
restorePages
} from './utils.js'
import type { InfinitePages } from './client.js'

Expand Down Expand Up @@ -75,6 +77,64 @@ describe('query', () => {
expect(input).toEqual([3, 1, 2])
expect(sort(null, (a: number, b: number) => a - b)).toBe(null)
})

describe('restore', () => {
const isTwo = (value: number) => value === 2

it('should put a value back at its previous index', () => {
expect(restore([1, 3], [1, 2, 3], isTwo)).toEqual([1, 2, 3])
})

it('should keep values written since', () => {
expect(restore([1, 3, 4], [1, 2, 3], isTwo)).toEqual([1, 2, 3, 4])
})

it('should put back every removed value', () => {
const isEven = (value: number) => value % 2 === 0

expect(restore([1, 3, 5], [1, 2, 3, 4, 5, 6], isEven)).toEqual([1, 2, 3, 4, 5, 6])
})

it('should put a value back into an empty array', () => {
expect(restore([], [2], isTwo)).toEqual([2])
})

it('should put a value at the end when the array became shorter', () => {
expect(restore([1], [1, 3, 4, 2], isTwo)).toEqual([1, 2])
})

it('should not mutate arrays', () => {
const array = [1, 3]
const prev = [1, 2, 3]

restore(array, prev, isTwo)

expect(array).toEqual([1, 3])
expect(prev).toEqual([1, 2, 3])
})

it('should keep the array when it holds the value again', () => {
const array = [2, 1, 3]

expect(restore(array, [1, 2, 3], isTwo)).toBe(array)
})

it('should keep the array when the previous one did not hold the value', () => {
const array = [1, 3]

expect(restore(array, [1, 3], isTwo)).toBe(array)
})

it('should keep the array when there is no previous one', () => {
const array = [1, 3]

expect(restore<number[] | null>(array, null, isTwo)).toBe(array)
})

it('should keep the empty value', () => {
expect(restore<number[] | null>(null, [1, 2, 3], isTwo)).toBe(null)
})
})
})

describe('page helpers', () => {
Expand Down Expand Up @@ -201,6 +261,124 @@ describe('query', () => {
})
)).toBe(null)
})

describe('restorePages', () => {
const isFour = (value: number) => value === 4
const removed = mapPages(pages, page => ({
items: drop(page.items, isFour)
}))
const adapter = (restoreItems: (items: number[]) => number[], page: Page) => ({
items: restoreItems(page.items)
})

it('should put an item back into the page it stood in', () => {
expect(restorePages(removed, pages, isFour, adapter)).toEqual(pages)
})

it('should keep items written since', () => {
const current = mapFirstPage(removed, page => ({
items: prepend(page.items, 0)
}))

expect(restorePages(current, pages, isFour, adapter).pages).toEqual([
{
items: [0, 1, 2]
},
{
items: [3, 4]
},
{
items: [5, 6]
}
])
})

it('should keep the pagination of the current pages', () => {
const result = restorePages({
...removed,
next: 4,
more: false
}, pages, isFour, adapter)

expect(result.next).toBe(4)
expect(result.more).toBe(false)
})

it('should put back items of several pages', () => {
const isEven = (value: number) => value % 2 === 0
const current = mapPages(pages, page => ({
items: drop(page.items, isEven)
}))

expect(restorePages(current, pages, isEven, adapter)).toEqual(pages)
})

it('should adapt a page shape', () => {
interface PostsPage {
posts: number[]
cursor: number
}

const page = (posts: number[]): PostsPage => ({
posts,
cursor: 1
})
const pagesOf = (posts: number[]): InfinitePages<PostsPage, number> => ({
pages: [page(posts)],
next: 2,
more: true
})

expect(restorePages(pagesOf([3]), pagesOf([3, 4]), isFour, (restoreItems, page) => ({
...page,
posts: restoreItems(page.posts)
}))).toEqual(pagesOf([3, 4]))
})

it('should keep pages when an item is back on another page', () => {
const current = {
...removed,
pages: [
{
items: [4]
},
...removed.pages
]
}

expect(restorePages(current, pages, isFour, adapter)).toBe(current)
})

it('should keep a page loaded since', () => {
const page = {
items: [7, 8]
}
const result = restorePages({
...removed,
pages: [...removed.pages, page]
}, pages, isFour, adapter)

expect(result.pages.at(-1)).toBe(page)
expect(result.pages[1].items).toEqual([3, 4])
})

it('should not put an item back when its page is no longer loaded', () => {
const current = {
...removed,
pages: removed.pages.slice(0, 1)
}

expect(restorePages(current, pages, isFour, adapter)).toEqual(current)
})

it('should keep pages when there are no previous ones', () => {
expect(restorePages<InfinitePages<Page, number> | null, number>(removed, null, isFour, adapter)).toBe(removed)
})

it('should keep empty pages value', () => {
expect(restorePages<InfinitePages<Page, number> | null, number>(null, pages, isFour, adapter)).toBe(null)
})
})
})
})
})
65 changes: 65 additions & 0 deletions packages/query/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,29 @@ export function drop<A extends unknown[] | EmptyValue>(
return (array ? (array as []).filter((value, index) => !what(value, index)) : array) as A
}

/**
* Put values removed with `drop` back at their previous indexes in the current array.
* It is the `undo` of a removal for `$data`: unlike a plain revert, it keeps what was written since.
* An array that holds a matching value again stays as it is, so nothing comes back twice.
* @param array - Current array or empty value.
* @param prev - Array before the removal or empty value.
* @param what - Predicate that returns true for removed values.
* @returns Array with the values back, or the input array when there is nothing to put back.
*/
/* @__NO_SIDE_EFFECTS__ */
export function restore<A extends unknown[] | EmptyValue>(
array: A,
prev: A,
what: (value: ArrayItem<A>) => boolean
) {
return (array && prev && !(array as []).some(what)
? (prev as []).reduce(
(next: unknown[], value, index) => (what(value) ? next.toSpliced(index, 0, value) : next),
array
)
: array) as A
}

/**
* Map values in an array-like cache value.
* @param array - Previous array or empty value.
Expand Down Expand Up @@ -216,3 +239,45 @@ export function sortPages<P extends InfinitePages<unknown, unknown> | EmptyValue
)
} as P
}

/**
* Put items removed with `drop` back into the pages they stood in, in the current pages.
* It is the `undo` of a removal for `$data`: unlike a plain revert, it keeps what was written since.
* Pages stay as they are when a matching item is back on any of them, as a refetch can do,
* and a page that is no longer loaded gets nothing back.
* The callback adapts a page shape to the item list, as in `sortPages`.
* It is called first to read items, then again to write items back.
* @param pages - Current infinite pages data or empty value.
* @param prev - Infinite pages data before the removal or empty value.
* @param what - Predicate that returns true for removed items.
* @param callback - Page adapter that receives an item-list mapper and returns an updated page.
* @returns Infinite pages with the items back, or the input value when there is nothing to put back.
*/
/* @__NO_SIDE_EFFECTS__ */
export function restorePages<P extends InfinitePages<unknown, unknown> | EmptyValue, I>(
pages: P,
prev: P,
what: (item: I) => boolean,
callback: (
restore: (items: I[]) => I[],
page: PagesItem<P>
) => PagesItem<P>
) {
const read = (page: PagesItem<P>) => {
let items: I[]

callback(i => items = i, page)

return items!
}

return !prev || pages?.pages.some(page => read(page as PagesItem<P>).some(what))
? pages
: mapPages(pages, (page, index) => {
const prevPage = prev.pages[index] as PagesItem<P> | undefined

return prevPage
? callback(items => restore(items, read(prevPage), what), page)
: page
})
}
Loading
Loading