Skip to content

Commit 8e035d3

Browse files
committed
ref: Reorganise lib code
- Move shared code into src/lib, leaving only features at the top level - Move the throttle queue into a class to help with testing & multi-storage support later - Add stubs for debounce queue
1 parent df6340b commit 8e035d3

33 files changed

+476
-303
lines changed

packages/nuqs/src/adapters/custom.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { renderQueryString } from '../url-encoding'
1+
export { renderQueryString } from '../lib/url-encoding'
22
export {
33
createAdapterProvider as unstable_createAdapterProvider,
44
type AdapterContext as unstable_AdapterContext

packages/nuqs/src/adapters/lib/context.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createContext, createElement, useContext, type ReactNode } from 'react'
2-
import { error } from '../../errors'
2+
import { error } from '../../lib/errors'
33
import type { UseAdapterHook } from './defs'
44

55
export type AdapterContext = {

packages/nuqs/src/adapters/lib/patch-history.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Emitter } from 'mitt'
2-
import { debug } from '../../debug'
3-
import { error } from '../../errors'
2+
import { debug } from '../../lib/debug'
3+
import { error } from '../../lib/errors'
44

55
export type SearchParamsSyncEmitter = Emitter<{ update: URLSearchParams }>
66

packages/nuqs/src/adapters/lib/react-router.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import mitt from 'mitt'
22
import { startTransition, useCallback, useEffect, useState } from 'react'
3-
import { renderQueryString } from '../../url-encoding'
3+
import { renderQueryString } from '../../lib/url-encoding'
44
import { createAdapterProvider } from './context'
55
import type { AdapterInterface, AdapterOptions } from './defs'
66
import {

packages/nuqs/src/adapters/next/impl.app.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useRouter, useSearchParams } from 'next/navigation'
22
import { startTransition, useCallback, useOptimistic } from 'react'
3-
import { debug } from '../../debug'
3+
import { debug } from '../../lib/debug'
44
import type { AdapterInterface, UpdateUrlFunction } from '../lib/defs'
55
import { renderURL } from './shared'
66

packages/nuqs/src/adapters/next/impl.pages.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useSearchParams } from 'next/navigation.js'
22
import type { NextRouter } from 'next/router'
33
import { useCallback } from 'react'
4-
import { debug } from '../../debug'
4+
import { debug } from '../../lib/debug'
55
import { createAdapterProvider } from '../lib/context'
66
import type { AdapterInterface, UpdateUrlFunction } from '../lib/defs'
77
import { renderURL } from './shared'

packages/nuqs/src/adapters/next/shared.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { renderQueryString } from '../../url-encoding'
1+
import { renderQueryString } from '../../lib/url-encoding'
22

33
export function renderURL(base: string, search: URLSearchParams) {
44
const hashlessBase = base.split('#')[0] ?? ''

packages/nuqs/src/adapters/react.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import mitt from 'mitt'
22
import { useEffect, useState } from 'react'
3-
import { renderQueryString } from '../url-encoding'
3+
import { renderQueryString } from '../lib/url-encoding'
44
import { createAdapterProvider } from './lib/context'
5-
import type { AdapterOptions } from './lib/defs'
5+
import type { AdapterInterface, AdapterOptions } from './lib/defs'
66
import { patchHistory, type SearchParamsSyncEmitter } from './lib/patch-history'
77

88
const emitter: SearchParamsSyncEmitter = mitt()
@@ -16,7 +16,7 @@ function updateUrl(search: URLSearchParams, options: AdapterOptions) {
1616
emitter.emit('update', search)
1717
}
1818

19-
function useNuqsReactAdapter() {
19+
function useNuqsReactAdapter(): AdapterInterface {
2020
const [searchParams, setSearchParams] = useState(() => {
2121
if (typeof location === 'undefined') {
2222
return new URLSearchParams()

packages/nuqs/src/adapters/testing.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createElement, type ReactNode } from 'react'
2-
import { resetQueue } from '../update-queue'
3-
import { renderQueryString } from '../url-encoding'
2+
import { globalThrottleQueue } from '../lib/queues/throttle'
3+
import { renderQueryString } from '../lib/url-encoding'
44
import { context } from './lib/context'
55
import type { AdapterInterface, AdapterOptions } from './lib/defs'
66

@@ -25,7 +25,7 @@ export function NuqsTestingAdapter({
2525
...props
2626
}: TestingAdapterProps) {
2727
if (resetUrlUpdateQueueOnMount) {
28-
resetQueue()
28+
globalThrottleQueue.reset()
2929
}
3030
const useAdapter = (): AdapterInterface => ({
3131
searchParams: new URLSearchParams(props.searchParams),

packages/nuqs/src/cache.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @ts-ignore
22
import { cache } from 'react'
33
import type { SearchParams, UrlKeys } from './defs'
4-
import { error } from './errors'
4+
import { error } from './lib/errors'
55
import { createLoader } from './loader'
66
import type { inferParserType, ParserMap } from './parsers'
77

packages/nuqs/src/update-queue.test.ts packages/nuqs/src/lib/compose.test.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { describe, expect, test, vi } from 'vitest'
2-
import { compose } from './update-queue'
1+
import { describe, expect, it, vi } from 'vitest'
2+
import { compose } from './compose'
33

4-
describe('update-queue/compose', () => {
5-
test('empty array', () => {
4+
describe('queues: compose', () => {
5+
it('handles an empty array', () => {
66
const final = vi.fn()
77
compose([], final)
88
expect(final).toHaveBeenCalledOnce()
99
})
10-
test('one item', () => {
10+
it('handles one item, calling it before the final', () => {
1111
const a = vi
1212
.fn()
1313
.mockImplementation(x => x())
@@ -20,7 +20,7 @@ describe('update-queue/compose', () => {
2020
final.mock.invocationCallOrder[0]!
2121
)
2222
})
23-
test('several items', () => {
23+
it('composes several items, calling them in order', () => {
2424
const a = vi.fn().mockImplementation(x => x())
2525
const b = vi.fn().mockImplementation(x => x())
2626
const c = vi.fn().mockImplementation(x => x())

packages/nuqs/src/lib/compose.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export function compose(
2+
fns: React.TransitionStartFunction[],
3+
final: () => void
4+
) {
5+
const recursiveCompose = (index: number) => {
6+
if (index === fns.length) {
7+
return final()
8+
}
9+
const fn = fns[index]
10+
if (!fn) {
11+
throw new Error('Invalid transition function')
12+
}
13+
fn(() => recursiveCompose(index + 1))
14+
}
15+
recursiveCompose(0)
16+
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import { DebouncedPromiseQueue } from './debounce'
3+
4+
describe('queues: DebouncedPromiseQueue', () => {
5+
it('creates a queue for a given key', () => {
6+
vi.useFakeTimers()
7+
const spy = vi.fn()
8+
const queue = new DebouncedPromiseQueue('key', spy)
9+
queue.push('value', 100)
10+
vi.advanceTimersToNextTimer()
11+
expect(spy).toHaveBeenCalledExactlyOnceWith('key', 'value')
12+
})
13+
it('debounces the queue', () => {
14+
vi.useFakeTimers()
15+
const spy = vi.fn()
16+
const queue = new DebouncedPromiseQueue('key', spy)
17+
queue.push('a', 100)
18+
queue.push('b', 100)
19+
queue.push('c', 100)
20+
vi.advanceTimersToNextTimer()
21+
expect(spy).toHaveBeenCalledExactlyOnceWith('key', 'c')
22+
})
23+
it('returns a stable promise to the next time the callback is called', async () => {
24+
vi.useFakeTimers()
25+
const queue = new DebouncedPromiseQueue('key', () => 'output')
26+
const p1 = queue.push('value', 100)
27+
const p2 = queue.push('value', 100)
28+
expect(p1).toBe(p2)
29+
vi.advanceTimersToNextTimer()
30+
await expect(p1).resolves.toBe('output')
31+
})
32+
it('returns a new Promise once the callback is called', async () => {
33+
vi.useFakeTimers()
34+
let count = 0
35+
const queue = new DebouncedPromiseQueue('key', () => count++)
36+
const p1 = queue.push('value', 100)
37+
vi.advanceTimersToNextTimer()
38+
await expect(p1).resolves.toBe(0)
39+
const p2 = queue.push('value', 100)
40+
expect(p2).not.toBe(p1)
41+
vi.advanceTimersToNextTimer()
42+
await expect(p2).resolves.toBe(1)
43+
})
44+
})
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { timeout } from '../timeout'
2+
import { withResolvers } from '../with-resolvers'
3+
4+
export class DebouncedPromiseQueue<ValueType, OutputType> {
5+
key: string
6+
callback: (key: string, value: ValueType) => OutputType
7+
resolvers = withResolvers<OutputType>()
8+
controller = new AbortController()
9+
10+
constructor(
11+
key: string,
12+
callback: (key: string, value: ValueType) => OutputType
13+
) {
14+
this.key = key
15+
this.callback = callback
16+
}
17+
18+
public push(value: ValueType, timeMs: number) {
19+
this.controller.abort()
20+
this.controller = new AbortController()
21+
timeout(
22+
() => {
23+
try {
24+
const output = this.callback(this.key, value)
25+
this.resolvers.resolve(output)
26+
} catch (error) {
27+
this.resolvers.reject(error)
28+
} finally {
29+
this.resolvers = withResolvers<OutputType>()
30+
}
31+
},
32+
timeMs,
33+
this.controller.signal
34+
)
35+
return this.resolvers.promise
36+
}
37+
}

0 commit comments

Comments
 (0)