Skip to content

Commit

Permalink
feat: Cache returned promise until next flush
Browse files Browse the repository at this point in the history
  • Loading branch information
franky47 committed Sep 11, 2023
1 parent f4817f5 commit aa0619b
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 32 deletions.
38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,29 +264,43 @@ const MultipleQueriesDemo = () => {
}
```

<!-- todo: All promises of a single update should be the same reference.
If you wish to know when the URL has been updated, you can await the
first returned Promise, which gives you the updated URLSearchParameters
object:
If you wish to know when the URL has been updated, and what it contains, you can
await the Promise returned by the state updater function, which gives you the
updated URLSearchParameters object:

```ts
const randomCoordinates = React.useCallback(() => {
// Always return the first promise
const promise = setLat(42)
// Queue up more state updates **synchronously**
setLng(12)
return promise
setLat(42)
return setLng(12)
}, [])

randomCoordinates().then((search: URLSearchParams) => {
search.get('lat') // 42
search.get('lng') // 12, has been queued and batch-updated
})
``` -->
```

<details>
<summary><em>Implementation details (Promise caching)</em></summary>

The returned Promise is cached until the next flush to the URL occurs,
so all calls to a setState (of any hook) in the same event loop tick will
return the same Promise reference.

Due to throttling of calls to the Web History API, the Promise may be cached
for several ticks. Batched updates will be merged and flushed once to the URL.
This means not every setState will reflect to the URL, if another one comes
overriding it before flush occurs.

The returned React state will reflect all set values instantly,
to keep UI responsive.

---

</details>

For query keys that should always move together, you can use `useQueryStates`
with an object containing each key's type, for a better DX:
with an object containing each key's type:

```ts
import { useQueryStates, parseAsFloat } from 'next-usequerystate'
Expand Down
7 changes: 5 additions & 2 deletions src/app/demos/batching/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ export default function BuilderPatternDemoPage() {
onClick={async () => {
// Call as many state updates as needed in the same event loop tick,
// and they will be asynchronously batched into one update.
setLat(Math.random() * 180 - 90)
setLng(Math.random() * 360 - 180)
const p1 = setLat(Math.random() * 180 - 90)
const p2 = setLng(Math.random() * 360 - 180)
// The returned promise is cached until next flush to the URL occurs
console.log('Ref eq: ', p1 === p2)
p1.then(search => console.log('Awaited: %s', search.toString()))
}}
>
Random coordinate
Expand Down
41 changes: 23 additions & 18 deletions src/lib/update-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type UpdateQueueItem = {

let updateQueue: UpdateQueueItem[] = []
let lastFlushTimestamp = 0
let flushPromiseCache: Promise<URLSearchParams> | null = null

export function enqueueQueryStringUpdate<Value>(
key: string,
Expand Down Expand Up @@ -40,24 +41,28 @@ export function enqueueQueryStringUpdate<Value>(
* @returns a Promise to the URLSearchParams that have been applied.
*/
export function flushToURL(router: Router) {
return new Promise<URLSearchParams>((resolve, reject) => {
const now = performance.now()
const timeSinceLastFlush = now - lastFlushTimestamp
const flushInMs = Math.max(
0,
Math.min(FLUSH_RATE_LIMIT_MS, FLUSH_RATE_LIMIT_MS - timeSinceLastFlush)
)
// console.debug('Scheduling flush in %f ms', flushInMs)
setTimeout(() => {
lastFlushTimestamp = performance.now()
const search = flushUpdateQueue(router)
if (!search) {
reject()
} else {
resolve(search)
}
}, flushInMs)
})
if (flushPromiseCache === null) {
flushPromiseCache = new Promise<URLSearchParams>((resolve, reject) => {
const now = performance.now()
const timeSinceLastFlush = now - lastFlushTimestamp
const flushInMs = Math.max(
0,
Math.min(FLUSH_RATE_LIMIT_MS, FLUSH_RATE_LIMIT_MS - timeSinceLastFlush)
)
// console.debug('Scheduling flush in %f ms', flushInMs)
setTimeout(() => {
lastFlushTimestamp = performance.now()
const search = flushUpdateQueue(router)
if (!search) {
reject()
} else {
resolve(search)
}
flushPromiseCache = null
}, flushInMs)
})
}
return flushPromiseCache
}

function flushUpdateQueue(router: Router) {
Expand Down

0 comments on commit aa0619b

Please sign in to comment.