forked from misskey-dev/misskey
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
137 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
export type FetchFunction<K, V> = (key: K) => Promise<V>; | ||
type ResolveReject<V> = Parameters<ConstructorParameters<typeof Promise<V>>[0]>; | ||
type ResolverPair<V> = { | ||
resolve: ResolveReject<V>[0]; | ||
reject: ResolveReject<V>[1]; | ||
}; | ||
export class DebounceLoader<K, V> { | ||
private resolverMap = new Map<K, ResolverPair<V>>(); | ||
private promiseMap = new Map<K, Promise<V>>(); | ||
private resolvedPromise = Promise.resolve(); | ||
constructor(private loadFn: FetchFunction<K, V>) {} | ||
|
||
public load(key: K): Promise<V> { | ||
const promise = this.promiseMap.get(key); | ||
if (typeof promise !== 'undefined') { | ||
return promise; | ||
} | ||
|
||
const isFirst = this.promiseMap.size === 0; | ||
const newPromise = new Promise<V>((resolve, reject) => { | ||
this.resolverMap.set(key, { resolve, reject }); | ||
}); | ||
this.promiseMap.set(key, newPromise); | ||
|
||
if (isFirst) { | ||
this.enqueueDebouncedLoadJob(); | ||
} | ||
|
||
return newPromise; | ||
} | ||
|
||
private runDebouncedLoad(): void { | ||
const resolvers = [...this.resolverMap]; | ||
this.resolverMap.clear(); | ||
this.promiseMap.clear(); | ||
|
||
for (const [key, { resolve, reject }] of resolvers) { | ||
this.loadFn(key).then(resolve, reject); | ||
} | ||
} | ||
|
||
private enqueueDebouncedLoadJob(): void { | ||
this.resolvedPromise.then(() => { | ||
process.nextTick(() => { | ||
this.runDebouncedLoad(); | ||
}); | ||
}); | ||
} | ||
} |
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,88 @@ | ||
import { DebounceLoader } from '@/misc/loader.js'; | ||
|
||
class Mock { | ||
loadCountByKey = new Map<number, number>(); | ||
load = async (key: number): Promise<number> => { | ||
const count = this.loadCountByKey.get(key); | ||
if (typeof count === 'undefined') { | ||
this.loadCountByKey.set(key, 1); | ||
} else { | ||
this.loadCountByKey.set(key, count + 1); | ||
} | ||
return key * 2; | ||
}; | ||
reset() { | ||
this.loadCountByKey.clear(); | ||
} | ||
} | ||
|
||
describe(DebounceLoader, () => { | ||
describe('single request', () => { | ||
it('loads once', async () => { | ||
const mock = new Mock(); | ||
const loader = new DebounceLoader(mock.load); | ||
expect(await loader.load(7)).toBe(14); | ||
expect(mock.loadCountByKey.size).toBe(1); | ||
expect(mock.loadCountByKey.get(7)).toBe(1); | ||
}); | ||
}); | ||
|
||
describe('two duplicated requests at same time', () => { | ||
it('loads once', async () => { | ||
const mock = new Mock(); | ||
const loader = new DebounceLoader(mock.load); | ||
const [v1, v2] = await Promise.all([ | ||
loader.load(7), | ||
loader.load(7), | ||
]); | ||
expect(v1).toBe(14); | ||
expect(v2).toBe(14); | ||
expect(mock.loadCountByKey.size).toBe(1); | ||
expect(mock.loadCountByKey.get(7)).toBe(1); | ||
}); | ||
}); | ||
|
||
describe('two different requests at same time', () => { | ||
it('loads twice', async () => { | ||
const mock = new Mock(); | ||
const loader = new DebounceLoader(mock.load); | ||
const [v1, v2] = await Promise.all([ | ||
loader.load(7), | ||
loader.load(13), | ||
]); | ||
expect(v1).toBe(14); | ||
expect(v2).toBe(26); | ||
expect(mock.loadCountByKey.size).toBe(2); | ||
expect(mock.loadCountByKey.get(7)).toBe(1); | ||
expect(mock.loadCountByKey.get(13)).toBe(1); | ||
}); | ||
}); | ||
|
||
describe('non-continuous same two requests', () => { | ||
it('loads twice', async () => { | ||
const mock = new Mock(); | ||
const loader = new DebounceLoader(mock.load); | ||
expect(await loader.load(7)).toBe(14); | ||
expect(mock.loadCountByKey.size).toBe(1); | ||
expect(mock.loadCountByKey.get(7)).toBe(1); | ||
mock.reset(); | ||
expect(await loader.load(7)).toBe(14); | ||
expect(mock.loadCountByKey.size).toBe(1); | ||
expect(mock.loadCountByKey.get(7)).toBe(1); | ||
}); | ||
}); | ||
|
||
describe('non-continuous different two requests', () => { | ||
it('loads twice', async () => { | ||
const mock = new Mock(); | ||
const loader = new DebounceLoader(mock.load); | ||
expect(await loader.load(7)).toBe(14); | ||
expect(mock.loadCountByKey.size).toBe(1); | ||
expect(mock.loadCountByKey.get(7)).toBe(1); | ||
mock.reset(); | ||
expect(await loader.load(13)).toBe(26); | ||
expect(mock.loadCountByKey.size).toBe(1); | ||
expect(mock.loadCountByKey.get(13)).toBe(1); | ||
}); | ||
}); | ||
}); |