Skip to content

Commit 76a40f2

Browse files
committed
wip: unit tests
1 parent f3b829d commit 76a40f2

File tree

3 files changed

+284
-19
lines changed

3 files changed

+284
-19
lines changed

meteor/server/publications/lib/ReactiveCacheCollection.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo'
66

77
type Reaction = () => void
88

9-
// nocommit - Mongo.Collection will probably not work correctly in meteor 3, so this will likely need reimplementing
109
export class ReactiveCacheCollection<Document extends { _id: ProtectedString<any> }> {
1110
readonly #collection: Mongo.Collection<Document>
1211

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import { sleep } from '@sofie-automation/shared-lib/dist/lib/lib'
2+
import { PromiseDebounce } from '../debounce'
3+
4+
describe('PromiseDebounce', () => {
5+
beforeEach(() => {
6+
jest.useFakeTimers()
7+
})
8+
9+
it('trigger', async () => {
10+
const fn = jest.fn()
11+
const debounce = new PromiseDebounce(fn, 10)
12+
13+
// No promise returned
14+
expect(debounce.trigger()).toBe(undefined)
15+
// Not called yet
16+
expect(fn).toHaveBeenCalledTimes(0)
17+
18+
// Wait for a bit
19+
await jest.advanceTimersByTimeAsync(6)
20+
expect(fn).toHaveBeenCalledTimes(0)
21+
22+
// Wait a bit more
23+
await jest.advanceTimersByTimeAsync(6)
24+
expect(fn).toHaveBeenCalledTimes(1)
25+
26+
// No more calls
27+
fn.mockClear()
28+
await jest.advanceTimersByTimeAsync(50)
29+
expect(fn).toHaveBeenCalledTimes(0)
30+
})
31+
32+
it('call', async () => {
33+
const fn = jest.fn()
34+
const debounce = new PromiseDebounce(fn, 10)
35+
36+
const ps = debounce.call()
37+
expect(ps).not.toBe(undefined)
38+
// Not called yet
39+
expect(fn).toHaveBeenCalledTimes(0)
40+
41+
// Wait for a bit
42+
await jest.advanceTimersByTimeAsync(6)
43+
expect(fn).toHaveBeenCalledTimes(0)
44+
45+
// Wait a bit more
46+
await jest.advanceTimersByTimeAsync(6)
47+
expect(fn).toHaveBeenCalledTimes(1)
48+
49+
// Should resolve without any more timer ticking
50+
await expect(ps).resolves.toBe(undefined)
51+
52+
// No more calls
53+
fn.mockClear()
54+
await jest.advanceTimersByTimeAsync(50)
55+
expect(fn).toHaveBeenCalledTimes(0)
56+
})
57+
58+
it('cancelWaiting - trigger', async () => {
59+
const fn = jest.fn()
60+
const debounce = new PromiseDebounce(fn, 10)
61+
62+
// No promise returned
63+
expect(debounce.trigger()).toBe(undefined)
64+
// Not called yet
65+
expect(fn).toHaveBeenCalledTimes(0)
66+
67+
// Wait for a bit
68+
await jest.advanceTimersByTimeAsync(6)
69+
expect(fn).toHaveBeenCalledTimes(0)
70+
71+
// Cancel waiting
72+
debounce.cancelWaiting()
73+
74+
// Wait until the timer should have fired
75+
await jest.advanceTimersByTimeAsync(50)
76+
expect(fn).toHaveBeenCalledTimes(0)
77+
})
78+
79+
it('cancelWaiting - call', async () => {
80+
const fn = jest.fn()
81+
const debounce = new PromiseDebounce(fn, 10)
82+
83+
const ps = debounce.call()
84+
ps.catch(() => null) // Add an error handler
85+
expect(ps).not.toBe(undefined)
86+
// Not called yet
87+
expect(fn).toHaveBeenCalledTimes(0)
88+
89+
// Wait for a bit
90+
await jest.advanceTimersByTimeAsync(6)
91+
expect(fn).toHaveBeenCalledTimes(0)
92+
93+
// Cancel waiting
94+
debounce.cancelWaiting()
95+
96+
// Wait until the timer should have fired
97+
await jest.advanceTimersByTimeAsync(50)
98+
expect(fn).toHaveBeenCalledTimes(0)
99+
100+
// Should have rejected
101+
await expect(ps).rejects.toThrow('Cancelled')
102+
})
103+
104+
it('cancelWaiting - call with error', async () => {
105+
const fn = jest.fn()
106+
const debounce = new PromiseDebounce(fn, 10)
107+
108+
const ps = debounce.call()
109+
ps.catch(() => null) // Add an error handler
110+
expect(ps).not.toBe(undefined)
111+
// Not called yet
112+
expect(fn).toHaveBeenCalledTimes(0)
113+
114+
// Wait for a bit
115+
await jest.advanceTimersByTimeAsync(6)
116+
expect(fn).toHaveBeenCalledTimes(0)
117+
118+
// Cancel waiting
119+
debounce.cancelWaiting(new Error('Custom error'))
120+
121+
// Wait until the timer should have fired
122+
await jest.advanceTimersByTimeAsync(50)
123+
expect(fn).toHaveBeenCalledTimes(0)
124+
125+
// Should have rejected
126+
await expect(ps).rejects.toThrow('Custom error')
127+
})
128+
129+
it('trigger - multiple', async () => {
130+
const fn = jest.fn()
131+
const debounce = new PromiseDebounce<void, [number]>(fn, 10)
132+
133+
// No promise returned
134+
expect(debounce.trigger(1)).toBe(undefined)
135+
// Not called yet
136+
expect(fn).toHaveBeenCalledTimes(0)
137+
138+
// Wait for a bit
139+
await jest.advanceTimersByTimeAsync(6)
140+
expect(fn).toHaveBeenCalledTimes(0)
141+
142+
// Trigger again
143+
expect(debounce.trigger(3)).toBe(undefined)
144+
expect(debounce.trigger(5)).toBe(undefined)
145+
146+
// Wait until the timer should have fired
147+
await jest.advanceTimersByTimeAsync(50)
148+
expect(fn).toHaveBeenCalledTimes(1)
149+
expect(fn).toHaveBeenCalledWith(5)
150+
})
151+
152+
it('trigger - during slow execution', async () => {
153+
const fn = jest.fn(async () => sleep(100))
154+
const debounce = new PromiseDebounce<void, [number]>(fn, 10)
155+
156+
// No promise returned
157+
expect(debounce.trigger(1)).toBe(undefined)
158+
// Not called yet
159+
expect(fn).toHaveBeenCalledTimes(0)
160+
161+
// Wait for it to start executing
162+
await jest.advanceTimersByTimeAsync(50)
163+
expect(fn).toHaveBeenCalledTimes(1)
164+
expect(fn).toHaveBeenCalledWith(1)
165+
166+
// Trigger again
167+
fn.mockClear()
168+
expect(debounce.trigger(3)).toBe(undefined)
169+
await jest.advanceTimersByTimeAsync(20)
170+
expect(debounce.trigger(5)).toBe(undefined)
171+
172+
// Wait until the second timer timer should
173+
await jest.advanceTimersByTimeAsync(100)
174+
expect(fn).toHaveBeenCalledTimes(1)
175+
expect(fn).toHaveBeenCalledWith(5)
176+
})
177+
178+
it('call - return value', async () => {
179+
const fn = jest.fn(async (val) => {
180+
await sleep(100)
181+
return val
182+
})
183+
const debounce = new PromiseDebounce<number, [number]>(fn, 10)
184+
185+
const ps1 = debounce.call(1)
186+
expect(ps1).not.toBe(undefined)
187+
// Not called yet
188+
expect(fn).toHaveBeenCalledTimes(0)
189+
190+
// Wait for it to start executing
191+
await jest.advanceTimersByTimeAsync(50)
192+
expect(fn).toHaveBeenCalledTimes(1)
193+
expect(fn).toHaveBeenCalledWith(1)
194+
195+
// Trigger again
196+
fn.mockClear()
197+
const ps3 = debounce.call(3)
198+
await jest.advanceTimersByTimeAsync(20)
199+
const ps5 = debounce.call(5)
200+
201+
// Wait until the second timer timer should
202+
await jest.advanceTimersByTimeAsync(150)
203+
expect(fn).toHaveBeenCalledTimes(1)
204+
expect(fn).toHaveBeenCalledWith(5)
205+
206+
await expect(ps1).resolves.toBe(1)
207+
await expect(ps3).resolves.toBe(5)
208+
await expect(ps5).resolves.toBe(5)
209+
})
210+
211+
it('call - throw error', async () => {
212+
const fn = jest.fn(async (val) => {
213+
await sleep(100)
214+
throw new Error(`Bad value: ${val}`)
215+
})
216+
const debounce = new PromiseDebounce<number, [number]>(fn, 10)
217+
218+
const ps1 = debounce.call(1)
219+
ps1.catch(() => null) // Add an error handler
220+
expect(ps1).not.toBe(undefined)
221+
// Not called yet
222+
expect(fn).toHaveBeenCalledTimes(0)
223+
224+
// Wait for it to start executing
225+
await jest.advanceTimersByTimeAsync(50)
226+
expect(fn).toHaveBeenCalledTimes(1)
227+
expect(fn).toHaveBeenCalledWith(1)
228+
229+
// Trigger again
230+
fn.mockClear()
231+
const ps3 = debounce.call(3)
232+
ps3.catch(() => null) // Add an error handler
233+
await jest.advanceTimersByTimeAsync(20)
234+
const ps5 = debounce.call(5)
235+
ps5.catch(() => null) // Add an error handler
236+
237+
// Wait until the second timer timer should
238+
await jest.advanceTimersByTimeAsync(150)
239+
expect(fn).toHaveBeenCalledTimes(1)
240+
expect(fn).toHaveBeenCalledWith(5)
241+
242+
await expect(ps1).rejects.toThrow('Bad value: 1')
243+
await expect(ps3).rejects.toThrow('Bad value: 5')
244+
await expect(ps5).rejects.toThrow('Bad value: 5')
245+
})
246+
247+
it('canelWaiting - during slow execution', async () => {
248+
const fn = jest.fn(async () => sleep(100))
249+
const debounce = new PromiseDebounce<void, [number]>(fn, 10)
250+
251+
// No promise returned
252+
expect(debounce.trigger(1)).toBe(undefined)
253+
// Not called yet
254+
expect(fn).toHaveBeenCalledTimes(0)
255+
256+
// Wait for it to start executing
257+
await jest.advanceTimersByTimeAsync(50)
258+
expect(fn).toHaveBeenCalledTimes(1)
259+
expect(fn).toHaveBeenCalledWith(1)
260+
261+
// Trigger again
262+
fn.mockClear()
263+
expect(debounce.trigger(3)).toBe(undefined)
264+
await jest.advanceTimersByTimeAsync(20)
265+
expect(debounce.trigger(5)).toBe(undefined)
266+
267+
debounce.cancelWaiting()
268+
269+
// Wait until the second timer timer should
270+
await jest.advanceTimersByTimeAsync(100)
271+
expect(fn).toHaveBeenCalledTimes(0)
272+
})
273+
})

meteor/server/publications/lib/debounce.ts

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { Meteor } from 'meteor/meteor'
33
/**
44
* Based on https://github.com/sindresorhus/p-debounce
55
* With additional features:
6-
* - `cancel` method
6+
* - `cancelWaiting` method
7+
* - ensures only one execution in progress at a time
78
*/
89
export class PromiseDebounce<TResult = void, TArgs extends unknown[] = []> {
910
readonly #fn: (...args: TArgs) => Promise<TResult>
@@ -71,23 +72,16 @@ export class PromiseDebounce<TResult = void, TArgs extends unknown[] = []> {
7172
const listeners = this.#waitingListeners
7273
this.#waitingListeners = []
7374

74-
this.#fn(...args)
75-
.then((result) => {
75+
Promise.resolve()
76+
.then(async () => {
77+
const result = await this.#fn(...args)
7678
for (const listener of listeners) {
77-
try {
78-
listener.resolve(result)
79-
} catch (e) {
80-
// TODO - error?
81-
}
79+
listener.resolve(result)
8280
}
8381
})
8482
.catch((error) => {
8583
for (const listener of listeners) {
86-
try {
87-
listener.reject(error)
88-
} catch (e) {
89-
// TODO - error?
90-
}
84+
listener.reject(error)
9185
}
9286
})
9387
.finally(() => {
@@ -119,13 +113,12 @@ export class PromiseDebounce<TResult = void, TArgs extends unknown[] = []> {
119113

120114
error = error ?? new Error('Cancelled')
121115

122-
for (const listener of listeners) {
123-
try {
116+
// Inform the listeners in the next tick
117+
Meteor.defer(() => {
118+
for (const listener of listeners) {
124119
listener.reject(error)
125-
} catch (e) {
126-
// TODO - error?
127120
}
128-
}
121+
})
129122
}
130123
}
131124
}

0 commit comments

Comments
 (0)