File size: 4,351 Bytes
09aec41 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | /**
* Notifier: microtask/frame batching, rebuild-before-notify ordering,
* no-listener laziness, synchronous notifyNow, and unsubscribe.
*/
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Notifier } from '../src/client/sessions/notifier.ts'
const microtask = (): Promise<void> => new Promise((resolve) => { queueMicrotask(resolve) })
afterEach(() => {
vi.unstubAllGlobals()
})
describe('Session notifier', () => {
it('collapses N markDirty calls into one flush, rebuilding before notifying', async () => {
const order: string[] = []
const notifier = new Notifier(() => order.push('rebuild'))
notifier.subscribe(() => order.push('notify'))
notifier.markDirty()
notifier.markDirty()
notifier.markDirty()
expect(order).toEqual([]) // nothing until the microtask boundary
await microtask()
expect(order).toEqual(['rebuild', 'notify'])
})
it('skips rebuild with zero listeners and ensureFresh rebuilds lazily exactly once', async () => {
let rebuilds = 0
const notifier = new Notifier(() => { rebuilds++ })
notifier.markDirty()
await microtask()
expect(rebuilds).toBe(0) // lazy: kept dirty
notifier.ensureFresh()
expect(rebuilds).toBe(1)
notifier.ensureFresh()
expect(rebuilds).toBe(1) // clean: no second rebuild
})
it('notifyNow runs listeners synchronously (controlled-input contract)', () => {
const order: string[] = []
const notifier = new Notifier(() => order.push('rebuild'))
notifier.subscribe(() => order.push('notify'))
notifier.notifyNow()
expect(order).toEqual(['rebuild', 'notify']) // before returning, no microtask needed
})
it('notifyNow with zero listeners stays lazy like markDirty', () => {
let rebuilds = 0
const notifier = new Notifier(() => { rebuilds++ })
notifier.notifyNow()
expect(rebuilds).toBe(0)
notifier.ensureFresh()
expect(rebuilds).toBe(1)
})
it('a scheduled flush after notifyNow already flushed is a no-op', async () => {
let rebuilds = 0
const notifier = new Notifier(() => { rebuilds++ })
notifier.subscribe(() => undefined)
notifier.markDirty() // schedules the microtask flush
notifier.notifyNow() // flushes synchronously, clears dirty
await microtask() // the scheduled flush finds dirty=false
expect(rebuilds).toBe(1)
})
it('collapses frame-dirty changes into one cumulative frame publication', () => {
const frames: FrameRequestCallback[] = []
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
frames.push(callback)
return frames.length
})
const order: string[] = []
const notifier = new Notifier(() => order.push('rebuild'))
notifier.subscribe(() => order.push('notify'))
notifier.markFrameDirty()
notifier.markFrameDirty()
notifier.markFrameDirty()
expect(order).toEqual([])
expect(frames).toHaveLength(1)
frames.shift()!(0)
expect(order).toEqual(['rebuild', 'notify'])
})
it('lets a structural microtask publication supersede a pending frame', async () => {
const frames: FrameRequestCallback[] = []
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
frames.push(callback)
return frames.length
})
let notifications = 0
const notifier = new Notifier(() => undefined)
notifier.subscribe(() => { notifications++ })
notifier.markFrameDirty()
notifier.markDirty()
await microtask()
expect(notifications).toBe(1)
frames.shift()!(0)
expect(notifications).toBe(1)
})
it('falls back to microtask batching when animation frames are unavailable', async () => {
let notifications = 0
const notifier = new Notifier(() => undefined)
notifier.subscribe(() => { notifications++ })
notifier.markFrameDirty()
notifier.markFrameDirty()
expect(notifications).toBe(0)
await microtask()
expect(notifications).toBe(1)
})
it('unsubscribed listeners stop receiving notifications', async () => {
let calls = 0
const notifier = new Notifier(() => undefined)
const unsubscribe = notifier.subscribe(() => { calls++ })
notifier.notifyNow()
expect(calls).toBe(1)
unsubscribe()
notifier.markDirty()
await microtask()
notifier.notifyNow()
expect(calls).toBe(1)
})
})
|