-
Notifications
You must be signed in to change notification settings - Fork 26.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added pending wrapper to abstract pending state management
- Loading branch information
Showing
11 changed files
with
288 additions
and
222 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// This takes advantage of `Promise.withResolvers` which is polyfilled in | ||
// this imported module. | ||
import './polyfill-promise-with-resolvers' | ||
|
||
import { SchedulerFn } from '../server/lib/schedule-on-next-tick' | ||
|
||
type CacheKeyFn<K, C extends string | number | null> = ( | ||
key: K | ||
) => PromiseLike<C> | C | ||
|
||
type BatcherOptions<K, C extends string | number | null> = { | ||
cacheKeyFn?: CacheKeyFn<K, C> | ||
schedulerFn?: SchedulerFn<void> | ||
} | ||
|
||
type WorkFn<V, C> = ( | ||
resolve: (value: V | PromiseLike<V>) => void, | ||
key: C | ||
) => Promise<V> | ||
|
||
/** | ||
* A wrapper for a function that will only allow one call to the function to | ||
* execute at a time. | ||
*/ | ||
export class Batcher<K, V, C extends string | number | null> { | ||
private readonly pending = new Map<C, Promise<V>>() | ||
|
||
protected constructor( | ||
private readonly cacheKeyFn?: CacheKeyFn<K, C>, | ||
/** | ||
* A function that will be called to schedule the wrapped function to be | ||
* executed. This defaults to a function that will execute the function | ||
* immediately. | ||
*/ | ||
private readonly schedulerFn: SchedulerFn<void> = (fn) => fn() | ||
) {} | ||
|
||
/** | ||
* Creates a new instance of PendingWrapper. If the key extends a string or | ||
* number, the key will be used as the cache key. If the key is an object, a | ||
* cache key function must be provided. | ||
*/ | ||
public static create<K extends string | number | null, V>( | ||
options?: BatcherOptions<K, K> | ||
): Batcher<K, V, K> | ||
public static create<K, V, C extends string | number | null>( | ||
options: BatcherOptions<K, C> & | ||
Required<Pick<BatcherOptions<K, C>, 'cacheKeyFn'>> | ||
): Batcher<K, V, C> | ||
public static create<K, V, C extends string | number | null>( | ||
options?: BatcherOptions<K, C> | ||
): Batcher<K, V, C> { | ||
return new Batcher<K, V, C>(options?.cacheKeyFn, options?.schedulerFn) | ||
} | ||
|
||
/** | ||
* Wraps a function in a promise that will be resolved or rejected only once | ||
* for a given key. This will allow multiple calls to the function to be | ||
* made, but only one will be executed at a time. The result of the first | ||
* call will be returned to all callers. | ||
* | ||
* @param key the key to use for the cache | ||
* @param fn the function to wrap | ||
* @returns a promise that resolves to the result of the function | ||
*/ | ||
public async batch(key: K, fn: WorkFn<V, C>): Promise<V> { | ||
const cacheKey = (this.cacheKeyFn ? await this.cacheKeyFn(key) : key) as C | ||
if (cacheKey === null) { | ||
return fn(Promise.resolve, cacheKey) | ||
} | ||
|
||
const pending = this.pending.get(cacheKey) | ||
if (pending) return pending | ||
|
||
const { promise, resolve, reject } = Promise.withResolvers<V>() | ||
this.pending.set(cacheKey, promise) | ||
|
||
this.schedulerFn(async () => { | ||
try { | ||
const result = await fn(resolve, cacheKey) | ||
|
||
// Resolving a promise multiple times is a no-op, so we can safely | ||
// resolve all pending promises with the same result. | ||
resolve(result) | ||
} catch (err) { | ||
reject(err) | ||
} finally { | ||
this.pending.delete(cacheKey) | ||
} | ||
}) | ||
|
||
return promise | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// This adds a `Promise.withResolvers` polyfill. This will soon be adopted into | ||
// the spec. | ||
// | ||
// TODO: remove this polyfill when it is adopted into the spec. | ||
// | ||
// https://tc39.es/proposal-promise-with-resolvers/ | ||
// | ||
if ( | ||
!('withResolvers' in Promise) || | ||
typeof Promise.withResolvers !== 'function' | ||
) { | ||
Promise.withResolvers = <T>() => { | ||
let resolvers: { | ||
resolve: (value: T | PromiseLike<T>) => void | ||
reject: (reason: any) => void | ||
} | ||
|
||
// Create the promise and assign the resolvers to the object. | ||
const promise = new Promise<T>((resolve, reject) => { | ||
resolvers = { resolve, reject } | ||
}) | ||
|
||
// We know that resolvers is defined because the Promise constructor runs | ||
// synchronously. | ||
return { promise, resolve: resolvers!.resolve, reject: resolvers!.reject } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.