import { TypedEvent } from 'rettime' import { Interceptor, InterceptorReadyState } from './interceptor' it('nesting interceptors', async () => { const socketSetup = vi.fn() const protocolSetup = vi.fn() class SocketInterceptor extends Interceptor<{ data: TypedEvent }> { static symbol = Symbol.for('socket-interceptor') protected predicate(): boolean { return true } protected setup(): void { socketSetup() queueMicrotask(() => { this.emitter.emit(new TypedEvent('data', { data: 1 })) this.emitter.emit(new TypedEvent('data', { data: 'hello' })) this.emitter.emit(new TypedEvent('data', { data: 2 })) }) } } class ProtocolInterceptor extends Interceptor<{ request: TypedEvent }> { static symbol = Symbol.for('protocol-interceptor') protected predicate(): boolean { return true } protected setup(): void { protocolSetup() const socket = Interceptor.singleton(SocketInterceptor) socket.apply(this) this.subscriptions.push(() => { socket.dispose(this) }) const controller = new AbortController() this.subscriptions.push(() => controller.abort()) socket.on( 'data', ({ data }) => { this.emitter.emit(new TypedEvent('request', { data })) }, { signal: controller.signal } ) } } class NumberInterceptor extends Interceptor<{ number: TypedEvent }> { static symbol = Symbol.for('number-interceptor') protected predicate(): boolean { return true } protected setup(): void { const protocol = Interceptor.singleton(ProtocolInterceptor) protocol.apply(this) this.subscriptions.push(() => { protocol.dispose(this) }) const controller = new AbortController() this.subscriptions.push(() => controller.abort()) protocol.on( 'request', ({ data }) => { if (typeof data === 'number') { this.emitter.emit(new TypedEvent('number', { data })) } }, { signal: controller.signal } ) } } class StringInterceptor extends Interceptor<{ string: TypedEvent }> { static symbol = Symbol.for('string-interceptor') protected predicate(): boolean { return true } protected setup(): void { const protocol = Interceptor.singleton(ProtocolInterceptor) protocol.apply(this) this.subscriptions.push(() => { protocol.dispose(this) }) const controller = new AbortController() this.subscriptions.push(() => controller.abort()) protocol.on( 'request', ({ data }) => { if (typeof data === 'string') { this.emitter.emit(new TypedEvent('string', { data })) } }, { signal: controller.signal } ) } } const numberListener = vi.fn() const numberInterceptor = new NumberInterceptor() numberInterceptor.on('number', ({ data }) => numberListener(data)) numberInterceptor.apply() const stringListener = vi.fn() const stringInterceptor = new StringInterceptor() stringInterceptor.on('string', ({ data }) => stringListener(data)) stringInterceptor.apply() expect(socketSetup).toHaveBeenCalledOnce() expect(protocolSetup).toHaveBeenCalledOnce() await expect.poll(() => numberListener).toHaveBeenCalledTimes(2) const protocolInterceptor = Interceptor.singleton(ProtocolInterceptor) numberInterceptor.dispose() expect(protocolInterceptor.readyState).toBe(InterceptorReadyState.ACTIVE) stringInterceptor.dispose() expect(protocolInterceptor.readyState).toBe(InterceptorReadyState.DISPOSED) expect(numberListener).toHaveBeenNthCalledWith(1, 1) expect(numberListener).toHaveBeenNthCalledWith(2, 2) expect(stringListener).toHaveBeenCalledExactlyOnceWith('hello') }) it('treats an interceptor as a singleton via "Interceptor.singleton()"', () => { const setup = vi.fn() const dispose = vi.fn() class MyInterceptor extends Interceptor<{ test: TypedEvent }> { protected predicate(): boolean { return true } protected setup(): void { setup() } public dispose(): void { super.dispose() dispose() } } const interceptor = Interceptor.singleton(MyInterceptor) expect(setup).not.toHaveBeenCalled() interceptor.apply() expect(setup).toHaveBeenCalledOnce() { const interceptor = Interceptor.singleton(MyInterceptor) expect(setup).toHaveBeenCalledOnce() interceptor.dispose() expect(dispose).toHaveBeenCalledOnce() } interceptor.dispose() expect(dispose).toHaveBeenCalledTimes(2) }) it('keeps an interceptor active until all owners dispose', () => { const setup = vi.fn() const teardown = vi.fn() class MyInterceptor extends Interceptor<{}> { protected predicate(): boolean { return true } protected setup(): void { setup() this.subscriptions.push(teardown) } } const interceptor = new MyInterceptor() const primaryOwner = {} const secondaryOwner = {} interceptor.apply(primaryOwner) interceptor.apply(primaryOwner) interceptor.apply(secondaryOwner) expect(setup).toHaveBeenCalledOnce() interceptor.dispose(primaryOwner) expect(interceptor.readyState).toBe(InterceptorReadyState.ACTIVE) expect(teardown).not.toHaveBeenCalled() interceptor.dispose(secondaryOwner) expect(interceptor.readyState).toBe(InterceptorReadyState.DISPOSED) expect(teardown).toHaveBeenCalledOnce() }) it('treats repeated public application as one ownership lease', () => { class MyInterceptor extends Interceptor<{}> { protected predicate(): boolean { return true } protected setup(): void {} } const interceptor = new MyInterceptor() interceptor.apply() interceptor.apply() interceptor.dispose() expect(interceptor.readyState).toBe(InterceptorReadyState.DISPOSED) }) it('removes all listeners when the interceptor is disposed', () => { class MyInterceptor extends Interceptor<{ test: TypedEvent }> { protected predicate(): boolean { return true } protected setup(): void {} } const interceptor = new MyInterceptor() interceptor.apply() const listener = vi.fn() interceptor.on('test', listener) interceptor.dispose() interceptor['emitter'].emit(new TypedEvent('test')) expect(listener).not.toHaveBeenCalled() }) it('applies the interceptor after disposal', () => { class MyInterceptor extends Interceptor<{}> { protected predicate(): boolean { return true } protected setup(): void {} } const interceptor = new MyInterceptor() interceptor.apply() expect(interceptor.readyState).toBe(InterceptorReadyState.ACTIVE) interceptor.dispose() expect(interceptor.readyState).toBe(InterceptorReadyState.DISPOSED) interceptor.apply() expect(interceptor.readyState).toBe(InterceptorReadyState.ACTIVE) })