From 0fdaabaf3eafde734d20f3f44264863e013678e7 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Wed, 2 Sep 2026 10:32:09 +0200 Subject: [PATCH 01/11] prevent datacryptor errors flooding with rate limiter --- src/e2ee/worker/FrameCryptor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/e2ee/worker/FrameCryptor.ts b/src/e2ee/worker/FrameCryptor.ts index bc0b771a61..24201406d6 100644 --- a/src/e2ee/worker/FrameCryptor.ts +++ b/src/e2ee/worker/FrameCryptor.ts @@ -15,6 +15,7 @@ import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed- import { ENCRYPTION_ALGORITHM, IV_LENGTH, UNENCRYPTED_BYTES } from '../constants'; import { CryptorError, CryptorErrorReason } from '../errors'; import { type CryptorCallbacks, CryptorEvent } from '../events'; +import { ErrorRateLimiter } from './ErrorRateLimiter'; import type { DecodeRatchetOptions, KeyProviderOptions, From 8ebd62bd8362522bdc447bfaf29705db21d6b5a7 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Wed, 2 Sep 2026 11:00:41 +0200 Subject: [PATCH 02/11] finalization strategy for worker listeners --- src/e2ee/E2eeManager.ts | 70 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/src/e2ee/E2eeManager.ts b/src/e2ee/E2eeManager.ts index f2c30db573..d9d9b795c6 100644 --- a/src/e2ee/E2eeManager.ts +++ b/src/e2ee/E2eeManager.ts @@ -62,6 +62,7 @@ export interface BaseE2EEManager { keyIndex: number, ): Promise; on(event: E, listener: E2EEManagerCallbacks[E]): this; + dispose?(): void; } /** @@ -87,6 +88,20 @@ export class E2EEManager private dataChannelEncryptionEnabled: boolean; + private unsubscribeLogLevel?: () => void; + + /** + * Runs a cleanup callback once this manager is garbage collected. Lets the + * log-level listener (held in a module-global Set on the main-thread logger) + * fall out of scope even when the consumer forgets to call `dispose()`. + */ + private static disposeRegistry = + typeof FinalizationRegistry !== 'undefined' && + typeof WeakRef !== 'undefined' && + new FinalizationRegistry((cleanup: () => void) => { + cleanup(); + }); + constructor(options: E2EEManagerOptions, dcEncryptionEnabled: boolean) { super(); this.keyProvider = options.keyProvider; @@ -129,13 +144,62 @@ export class E2EEManager this.worker.onmessage = this.onWorkerMessage; this.worker.onerror = this.onWorkerError; this.worker.postMessage(msg); - onWorkerLogLevelChanged((level) => { - this.worker?.postMessage({ kind: 'setLogLevel', data: { level } }); - }); + this.subscribeToLogLevelChanges(); } } } + /** + * Subscribe the current worker to main-thread `workerLogger` level changes, + * without strongly retaining `this` or `this.worker` from the module-global + * listener Set on the logger. See {@link disposeRegistry}. + */ + private subscribeToLogLevelChanges() { + // Guard against duplicate registration on re-setup. + this.unsubscribeLogLevel?.(); + + let unsub: (() => void) | undefined; + if (E2EEManager.disposeRegistry) { + // Modern engines: hold the worker weakly so the module-global listener Set + // on the logger can't retain this manager, and clean up the entry on GC. + const workerRef = new WeakRef(this.worker); + unsub = onWorkerLogLevelChanged((level) => { + const worker = workerRef.deref(); + if (!worker) { + unsub?.(); + return; + } + worker.postMessage({ kind: 'setLogLevel', data: { level } }); + }); + E2EEManager.disposeRegistry.register(this, unsub, this); + } else { + // Safari <14.1 and similar: no WeakRef. Fall back to a strong reference; + // the leak lives until the consumer calls `dispose()`. + const worker = this.worker; + unsub = onWorkerLogLevelChanged((level) => { + worker.postMessage({ kind: 'setLogLevel', data: { level } }); + }); + } + this.unsubscribeLogLevel = unsub; + } + + /** + * Release the log-level subscription and detach the worker message handlers. + * The worker itself is caller-owned and is not terminated. Idempotent. + */ + dispose() { + this.unsubscribeLogLevel?.(); + this.unsubscribeLogLevel = undefined; + if (E2EEManager.disposeRegistry) { + E2EEManager.disposeRegistry.unregister(this); + } + if (this.worker) { + this.worker.onmessage = null; + this.worker.onerror = null; + } + this.removeAllListeners(); + } + /** * @internal */ From d402e06700fadee488c213c54bab81c2ebf3c0a4 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Wed, 2 Sep 2026 10:41:30 +0200 Subject: [PATCH 03/11] prettier --- src/e2ee/worker/FrameCryptor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/e2ee/worker/FrameCryptor.ts b/src/e2ee/worker/FrameCryptor.ts index 24201406d6..bc0b771a61 100644 --- a/src/e2ee/worker/FrameCryptor.ts +++ b/src/e2ee/worker/FrameCryptor.ts @@ -15,7 +15,6 @@ import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed- import { ENCRYPTION_ALGORITHM, IV_LENGTH, UNENCRYPTED_BYTES } from '../constants'; import { CryptorError, CryptorErrorReason } from '../errors'; import { type CryptorCallbacks, CryptorEvent } from '../events'; -import { ErrorRateLimiter } from './ErrorRateLimiter'; import type { DecodeRatchetOptions, KeyProviderOptions, From 941e550a2280bd6906225213d5a6d052000ca26c Mon Sep 17 00:00:00 2001 From: lukasIO Date: Wed, 2 Sep 2026 11:04:51 +0200 Subject: [PATCH 04/11] re-add logs --- src/e2ee/E2eeManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/e2ee/E2eeManager.ts b/src/e2ee/E2eeManager.ts index d9d9b795c6..2eebf74fe8 100644 --- a/src/e2ee/E2eeManager.ts +++ b/src/e2ee/E2eeManager.ts @@ -239,7 +239,7 @@ export class E2EEManager break; } } - + log.error(data.error.message); this.emit(EncryptionEvent.EncryptionError, data.error, data.participantIdentity); break; case 'initAck': From 2482aa8f8a7016400a60b01b18c482236c712061 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 3 Sep 2026 12:53:20 +0200 Subject: [PATCH 05/11] add tests and reject pending futures on dispose --- src/e2ee/E2eeManager.test.ts | 195 +++++++++++++++++++++++++++++++++++ src/e2ee/E2eeManager.ts | 19 +++- src/logger.ts | 5 + 3 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 src/e2ee/E2eeManager.test.ts diff --git a/src/e2ee/E2eeManager.test.ts b/src/e2ee/E2eeManager.test.ts new file mode 100644 index 0000000000..59bc12f0e3 --- /dev/null +++ b/src/e2ee/E2eeManager.test.ts @@ -0,0 +1,195 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { LogLevel, _getWorkerLogLevelListenerCount, setLogLevel, workerLogger } from '../logger'; +import Room from '../room/Room'; +import { E2EEManager } from './E2eeManager'; +import { BaseKeyProvider } from './KeyProvider'; + +/** + * Install just enough of the DOM to let isE2EESupported() return true so + * setup() doesn't throw. + */ +function installE2EEShims() { + const w = window as unknown as Record; + if (typeof w.RTCRtpSender === 'undefined') { + w.RTCRtpSender = class {}; + } + w.RTCRtpSender.prototype.createEncodedStreams = () => {}; +} + +class FakeWorker { + postMessage = vi.fn(); + + onmessage: unknown = null; + + onerror: unknown = null; + + levelMessages(): LogLevel[] { + return this.postMessage.mock.calls + .map(([m]) => m) + .filter((m: any) => m?.kind === 'setLogLevel') + .map((m: any) => m.data.level); + } +} + +function makeManager() { + installE2EEShims(); + const room = new Room(); + const worker = new FakeWorker(); + const manager = new E2EEManager( + { keyProvider: new BaseKeyProvider({ sharedKey: true }), worker: worker as unknown as Worker }, + false, + ); + return { room, worker, manager }; +} + +describe('E2EEManager log-level listener lifecycle', () => { + const startingLevel = workerLogger.getLevel(); + const startingCount = _getWorkerLogLevelListenerCount(); + + afterEach(() => { + setLogLevel(startingLevel); + }); + + it('forwards level changes to the worker while subscribed', () => { + const { room, worker, manager } = makeManager(); + manager.setup(room); + worker.postMessage.mockClear(); + + setLogLevel(LogLevel.debug); + + expect(worker.levelMessages()).toEqual([LogLevel.debug]); + manager.dispose(); + }); + + it('dispose() removes the listener and stops forwarding', () => { + const { room, worker, manager } = makeManager(); + manager.setup(room); + manager.dispose(); + worker.postMessage.mockClear(); + + setLogLevel(LogLevel.warn); + + expect(worker.postMessage).not.toHaveBeenCalled(); + expect(_getWorkerLogLevelListenerCount()).toBe(startingCount); + }); + + it('re-setup with a new room does not stack listeners', () => { + const { worker, manager } = makeManager(); + const roomA = new Room(); + const roomB = new Room(); + manager.setup(roomA); + const countAfterFirst = _getWorkerLogLevelListenerCount(); + manager.setup(roomB); + expect(_getWorkerLogLevelListenerCount()).toBe(countAfterFirst); + + worker.postMessage.mockClear(); + setLogLevel(LogLevel.debug); + expect(worker.levelMessages()).toEqual([LogLevel.debug]); // exactly one delivery + + manager.dispose(); + }); + + it('dispose() is idempotent', () => { + const { room, manager } = makeManager(); + manager.setup(room); + manager.dispose(); + manager.dispose(); + expect(_getWorkerLogLevelListenerCount()).toBe(startingCount); + }); + + it('dispose() rejects pending encrypt/decrypt futures and clears both maps', async () => { + const { room, manager } = makeManager(); + manager.setup(room); + + const encrypting = manager.encryptData(new Uint8Array([1, 2, 3]) as any); + const decrypting = manager.handleEncryptedData( + new Uint8Array([4, 5, 6]) as any, + new Uint8Array([7, 8, 9]) as any, + 'peer', + 0, + ); + + const priv = manager as unknown as { + encryptDataRequests: Map; + decryptDataRequests: Map; + }; + expect(priv.encryptDataRequests.size).toBe(1); + expect(priv.decryptDataRequests.size).toBe(1); + + manager.dispose(); + + await expect(encrypting).rejects.toThrow(/disposed/); + await expect(decrypting).rejects.toThrow(/disposed/); + expect(priv.encryptDataRequests.size).toBe(0); + expect(priv.decryptDataRequests.size).toBe(0); + + // Second dispose while maps are empty must not throw. + expect(() => manager.dispose()).not.toThrow(); + }); +}); + +/** + * GC-path test. Flaky by construction — FinalizationRegistry callbacks are + * best-effort. Skipped unless vitest is run with `--expose-gc`: + * + * NODE_OPTIONS="--expose-gc" pnpm exec vitest run src/e2ee/E2eeManager.test.ts + * + * Deliberately bypasses `manager.setup(room)`. `new Room()` on its own is not + * collectable in this test environment (device-change listeners, timers), and + * that leak is not what this test is about — it would only mask what we + * actually want to verify: that the log-level listener wiring holds nothing + * strongly. + */ +describe('E2EEManager GC cleanup', () => { + const startingLevel = workerLogger.getLevel(); + + beforeEach(() => { + installE2EEShims(); + }); + + afterEach(() => { + setLogLevel(startingLevel); + }); + + it.skipIf(!(globalThis as any).gc)( + 'releases the log-level listener when the manager is garbage collected', + async () => { + const before = _getWorkerLogLevelListenerCount(); + + // Construct + subscribe in an IIFE so nothing lives on the test's stack. + // Direct call to the private subscription — no Room, no leaky graph. + const managerRef = ((): WeakRef => { + const worker = new FakeWorker(); + const manager = new E2EEManager( + { + keyProvider: new BaseKeyProvider({ sharedKey: true }), + worker: worker as unknown as Worker, + }, + false, + ); + (manager as unknown as { subscribeToLogLevelChanges(): void }).subscribeToLogLevelChanges(); + expect(_getWorkerLogLevelListenerCount()).toBe(before + 1); + return new WeakRef(manager); + })(); + + // Full major GC + macrotask yield in a loop, with allocation pressure to + // force the major sweep FinalizationRegistry needs. + // + // Crucial: do NOT call `managerRef.deref()` inside the loop. Per spec, + // `WeakRef.prototype.deref` keeps the referent alive until the end of the + // current job — calling it in the check would pin the manager forever. + // Read the listener count (which does not touch the referent) instead. + const gc = (globalThis as any).gc as (opts?: { type?: 'major'; execution?: 'sync' }) => void; + for (let i = 0; i < 50; i++) { + void new Array(100_000).fill({ i }); + gc({ type: 'major', execution: 'sync' }); + await new Promise((r) => setImmediate(r)); + if (_getWorkerLogLevelListenerCount() === before) break; + } + + // Diagnostic: separate "manager wasn't collected" from "FR didn't fire". + expect(managerRef.deref(), 'manager was not collected — strong ref leaked').toBeUndefined(); + expect(_getWorkerLogLevelListenerCount()).toBe(before); + }, + ); +}); diff --git a/src/e2ee/E2eeManager.ts b/src/e2ee/E2eeManager.ts index 2eebf74fe8..c0d47def46 100644 --- a/src/e2ee/E2eeManager.ts +++ b/src/e2ee/E2eeManager.ts @@ -184,8 +184,10 @@ export class E2EEManager } /** - * Release the log-level subscription and detach the worker message handlers. - * The worker itself is caller-owned and is not terminated. Idempotent. + * @internal + * Release the log-level subscription, reject any pending encrypt/decrypt + * futures, and detach the worker message handlers. The worker itself is + * caller-owned and is not terminated. Idempotent. */ dispose() { this.unsubscribeLogLevel?.(); @@ -193,6 +195,19 @@ export class E2EEManager if (E2EEManager.disposeRegistry) { E2EEManager.disposeRegistry.unregister(this); } + + // Reject pending futures BEFORE detaching worker handlers, so any late + // response can't resolve one after we've cut the pipe. Each future's + // `onFinally` deletes its own map entry, so both maps drain themselves. + // Snapshot before iterating in case a rejection handler mutates the map. + const disposalError = new Error('E2EEManager disposed'); + for (const future of [...this.encryptDataRequests.values()]) { + future.reject?.(disposalError); + } + for (const future of [...this.decryptDataRequests.values()]) { + future.reject?.(disposalError); + } + if (this.worker) { this.worker.onmessage = null; this.worker.onerror = null; diff --git a/src/logger.ts b/src/logger.ts index d03cd24dc4..15443709ce 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -148,3 +148,8 @@ export function onWorkerLogLevelChanged(cb: (level: LogLevel) => void): () => vo workerLogLevelListeners.delete(cb); }; } + +/** @internal Test-only accessor: current number of workerLogger level listeners. */ +export function _getWorkerLogLevelListenerCount(): number { + return workerLogLevelListeners.size; +} From 28e9f18fefc9b86b1f3eeb9bf2eb87b61da58ccc Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 3 Sep 2026 12:55:39 +0200 Subject: [PATCH 06/11] lint --- src/e2ee/E2eeManager.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/e2ee/E2eeManager.test.ts b/src/e2ee/E2eeManager.test.ts index 59bc12f0e3..e48d15827c 100644 --- a/src/e2ee/E2eeManager.test.ts +++ b/src/e2ee/E2eeManager.test.ts @@ -181,6 +181,7 @@ describe('E2EEManager GC cleanup', () => { // Read the listener count (which does not touch the referent) instead. const gc = (globalThis as any).gc as (opts?: { type?: 'major'; execution?: 'sync' }) => void; for (let i = 0; i < 50; i++) { + // eslint-disable-next-line no-void void new Array(100_000).fill({ i }); gc({ type: 'major', execution: 'sync' }); await new Promise((r) => setImmediate(r)); From 165ff38033d53a0d4bc4591080afb07b70070b4d Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 3 Sep 2026 12:56:37 +0200 Subject: [PATCH 07/11] fix logger export --- src/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger.ts b/src/logger.ts index 15443709ce..a472d65af4 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -150,6 +150,6 @@ export function onWorkerLogLevelChanged(cb: (level: LogLevel) => void): () => vo } /** @internal Test-only accessor: current number of workerLogger level listeners. */ -export function _getWorkerLogLevelListenerCount(): number { +export function getWorkerLogLevelListenerCount(): number { return workerLogLevelListeners.size; } From bf9d7f1a0d4ac4441a034260f2f68fbfd9e22706 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 3 Sep 2026 12:59:00 +0200 Subject: [PATCH 08/11] fix import --- src/e2ee/E2eeManager.test.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/e2ee/E2eeManager.test.ts b/src/e2ee/E2eeManager.test.ts index e48d15827c..b42b19fbc9 100644 --- a/src/e2ee/E2eeManager.test.ts +++ b/src/e2ee/E2eeManager.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { LogLevel, _getWorkerLogLevelListenerCount, setLogLevel, workerLogger } from '../logger'; +import { LogLevel, getWorkerLogLevelListenerCount, setLogLevel, workerLogger } from '../logger'; import Room from '../room/Room'; import { E2EEManager } from './E2eeManager'; import { BaseKeyProvider } from './KeyProvider'; @@ -44,7 +44,7 @@ function makeManager() { describe('E2EEManager log-level listener lifecycle', () => { const startingLevel = workerLogger.getLevel(); - const startingCount = _getWorkerLogLevelListenerCount(); + const startingCount = getWorkerLogLevelListenerCount(); afterEach(() => { setLogLevel(startingLevel); @@ -70,7 +70,7 @@ describe('E2EEManager log-level listener lifecycle', () => { setLogLevel(LogLevel.warn); expect(worker.postMessage).not.toHaveBeenCalled(); - expect(_getWorkerLogLevelListenerCount()).toBe(startingCount); + expect(getWorkerLogLevelListenerCount()).toBe(startingCount); }); it('re-setup with a new room does not stack listeners', () => { @@ -78,9 +78,9 @@ describe('E2EEManager log-level listener lifecycle', () => { const roomA = new Room(); const roomB = new Room(); manager.setup(roomA); - const countAfterFirst = _getWorkerLogLevelListenerCount(); + const countAfterFirst = getWorkerLogLevelListenerCount(); manager.setup(roomB); - expect(_getWorkerLogLevelListenerCount()).toBe(countAfterFirst); + expect(getWorkerLogLevelListenerCount()).toBe(countAfterFirst); worker.postMessage.mockClear(); setLogLevel(LogLevel.debug); @@ -94,7 +94,7 @@ describe('E2EEManager log-level listener lifecycle', () => { manager.setup(room); manager.dispose(); manager.dispose(); - expect(_getWorkerLogLevelListenerCount()).toBe(startingCount); + expect(getWorkerLogLevelListenerCount()).toBe(startingCount); }); it('dispose() rejects pending encrypt/decrypt futures and clears both maps', async () => { @@ -154,7 +154,7 @@ describe('E2EEManager GC cleanup', () => { it.skipIf(!(globalThis as any).gc)( 'releases the log-level listener when the manager is garbage collected', async () => { - const before = _getWorkerLogLevelListenerCount(); + const before = getWorkerLogLevelListenerCount(); // Construct + subscribe in an IIFE so nothing lives on the test's stack. // Direct call to the private subscription — no Room, no leaky graph. @@ -168,7 +168,7 @@ describe('E2EEManager GC cleanup', () => { false, ); (manager as unknown as { subscribeToLogLevelChanges(): void }).subscribeToLogLevelChanges(); - expect(_getWorkerLogLevelListenerCount()).toBe(before + 1); + expect(getWorkerLogLevelListenerCount()).toBe(before + 1); return new WeakRef(manager); })(); @@ -185,12 +185,12 @@ describe('E2EEManager GC cleanup', () => { void new Array(100_000).fill({ i }); gc({ type: 'major', execution: 'sync' }); await new Promise((r) => setImmediate(r)); - if (_getWorkerLogLevelListenerCount() === before) break; + if (getWorkerLogLevelListenerCount() === before) break; } // Diagnostic: separate "manager wasn't collected" from "FR didn't fire". expect(managerRef.deref(), 'manager was not collected — strong ref leaked').toBeUndefined(); - expect(_getWorkerLogLevelListenerCount()).toBe(before); + expect(getWorkerLogLevelListenerCount()).toBe(before); }, ); }); From df9c4180d0f4b449e6c61e4eb883a7ba2b0c4665 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Mon, 7 Sep 2026 11:16:04 +0200 Subject: [PATCH 09/11] use cryptor error to signal data encryption errors on dispose --- src/e2ee/E2eeManager.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/e2ee/E2eeManager.ts b/src/e2ee/E2eeManager.ts index c0d47def46..d7b1c8cc12 100644 --- a/src/e2ee/E2eeManager.ts +++ b/src/e2ee/E2eeManager.ts @@ -25,6 +25,7 @@ import { import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays'; import type { BaseKeyProvider } from './KeyProvider'; import { E2EE_FLAG, E2EE_TRACK_ID } from './constants'; +import { CryptorError, CryptorErrorReason } from './errors'; import { type E2EEManagerCallbacks, EncryptionEvent, KeyProviderEvent } from './events'; import type { DecryptDataRequestMessage, @@ -200,7 +201,10 @@ export class E2EEManager // response can't resolve one after we've cut the pipe. Each future's // `onFinally` deletes its own map entry, so both maps drain themselves. // Snapshot before iterating in case a rejection handler mutates the map. - const disposalError = new Error('E2EEManager disposed'); + const disposalError = new CryptorError( + 'E2EEManager disposed', + CryptorErrorReason.InternalError, + ); for (const future of [...this.encryptDataRequests.values()]) { future.reject?.(disposalError); } From 6b2d59c81374c9dde7a39f3f32ca2a2a88ff8014 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 3 Sep 2026 12:53:20 +0200 Subject: [PATCH 10/11] add tests and reject pending futures on dispose --- src/e2ee/E2eeManager.test.ts | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/e2ee/E2eeManager.test.ts b/src/e2ee/E2eeManager.test.ts index b42b19fbc9..dbaa7943b2 100644 --- a/src/e2ee/E2eeManager.test.ts +++ b/src/e2ee/E2eeManager.test.ts @@ -1,5 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +<<<<<<< HEAD import { LogLevel, getWorkerLogLevelListenerCount, setLogLevel, workerLogger } from '../logger'; +======= +import { LogLevel, _getWorkerLogLevelListenerCount, setLogLevel, workerLogger } from '../logger'; +>>>>>>> 6c108c99 (add tests and reject pending futures on dispose) import Room from '../room/Room'; import { E2EEManager } from './E2eeManager'; import { BaseKeyProvider } from './KeyProvider'; @@ -44,7 +48,11 @@ function makeManager() { describe('E2EEManager log-level listener lifecycle', () => { const startingLevel = workerLogger.getLevel(); +<<<<<<< HEAD const startingCount = getWorkerLogLevelListenerCount(); +======= + const startingCount = _getWorkerLogLevelListenerCount(); +>>>>>>> 6c108c99 (add tests and reject pending futures on dispose) afterEach(() => { setLogLevel(startingLevel); @@ -70,7 +78,11 @@ describe('E2EEManager log-level listener lifecycle', () => { setLogLevel(LogLevel.warn); expect(worker.postMessage).not.toHaveBeenCalled(); +<<<<<<< HEAD expect(getWorkerLogLevelListenerCount()).toBe(startingCount); +======= + expect(_getWorkerLogLevelListenerCount()).toBe(startingCount); +>>>>>>> 6c108c99 (add tests and reject pending futures on dispose) }); it('re-setup with a new room does not stack listeners', () => { @@ -78,9 +90,15 @@ describe('E2EEManager log-level listener lifecycle', () => { const roomA = new Room(); const roomB = new Room(); manager.setup(roomA); +<<<<<<< HEAD const countAfterFirst = getWorkerLogLevelListenerCount(); manager.setup(roomB); expect(getWorkerLogLevelListenerCount()).toBe(countAfterFirst); +======= + const countAfterFirst = _getWorkerLogLevelListenerCount(); + manager.setup(roomB); + expect(_getWorkerLogLevelListenerCount()).toBe(countAfterFirst); +>>>>>>> 6c108c99 (add tests and reject pending futures on dispose) worker.postMessage.mockClear(); setLogLevel(LogLevel.debug); @@ -94,7 +112,11 @@ describe('E2EEManager log-level listener lifecycle', () => { manager.setup(room); manager.dispose(); manager.dispose(); +<<<<<<< HEAD expect(getWorkerLogLevelListenerCount()).toBe(startingCount); +======= + expect(_getWorkerLogLevelListenerCount()).toBe(startingCount); +>>>>>>> 6c108c99 (add tests and reject pending futures on dispose) }); it('dispose() rejects pending encrypt/decrypt futures and clears both maps', async () => { @@ -154,7 +176,11 @@ describe('E2EEManager GC cleanup', () => { it.skipIf(!(globalThis as any).gc)( 'releases the log-level listener when the manager is garbage collected', async () => { +<<<<<<< HEAD const before = getWorkerLogLevelListenerCount(); +======= + const before = _getWorkerLogLevelListenerCount(); +>>>>>>> 6c108c99 (add tests and reject pending futures on dispose) // Construct + subscribe in an IIFE so nothing lives on the test's stack. // Direct call to the private subscription — no Room, no leaky graph. @@ -168,7 +194,11 @@ describe('E2EEManager GC cleanup', () => { false, ); (manager as unknown as { subscribeToLogLevelChanges(): void }).subscribeToLogLevelChanges(); +<<<<<<< HEAD expect(getWorkerLogLevelListenerCount()).toBe(before + 1); +======= + expect(_getWorkerLogLevelListenerCount()).toBe(before + 1); +>>>>>>> 6c108c99 (add tests and reject pending futures on dispose) return new WeakRef(manager); })(); @@ -181,16 +211,27 @@ describe('E2EEManager GC cleanup', () => { // Read the listener count (which does not touch the referent) instead. const gc = (globalThis as any).gc as (opts?: { type?: 'major'; execution?: 'sync' }) => void; for (let i = 0; i < 50; i++) { +<<<<<<< HEAD // eslint-disable-next-line no-void void new Array(100_000).fill({ i }); gc({ type: 'major', execution: 'sync' }); await new Promise((r) => setImmediate(r)); if (getWorkerLogLevelListenerCount() === before) break; +======= + void new Array(100_000).fill({ i }); + gc({ type: 'major', execution: 'sync' }); + await new Promise((r) => setImmediate(r)); + if (_getWorkerLogLevelListenerCount() === before) break; +>>>>>>> 6c108c99 (add tests and reject pending futures on dispose) } // Diagnostic: separate "manager wasn't collected" from "FR didn't fire". expect(managerRef.deref(), 'manager was not collected — strong ref leaked').toBeUndefined(); +<<<<<<< HEAD expect(getWorkerLogLevelListenerCount()).toBe(before); +======= + expect(_getWorkerLogLevelListenerCount()).toBe(before); +>>>>>>> 6c108c99 (add tests and reject pending futures on dispose) }, ); }); From 98c266fa62866fdb462b0cd1e75caa44996de21c Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 3 Sep 2026 12:55:39 +0200 Subject: [PATCH 11/11] lint --- src/e2ee/E2eeManager.test.ts | 41 ------------------------------------ 1 file changed, 41 deletions(-) diff --git a/src/e2ee/E2eeManager.test.ts b/src/e2ee/E2eeManager.test.ts index dbaa7943b2..b42b19fbc9 100644 --- a/src/e2ee/E2eeManager.test.ts +++ b/src/e2ee/E2eeManager.test.ts @@ -1,9 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -<<<<<<< HEAD import { LogLevel, getWorkerLogLevelListenerCount, setLogLevel, workerLogger } from '../logger'; -======= -import { LogLevel, _getWorkerLogLevelListenerCount, setLogLevel, workerLogger } from '../logger'; ->>>>>>> 6c108c99 (add tests and reject pending futures on dispose) import Room from '../room/Room'; import { E2EEManager } from './E2eeManager'; import { BaseKeyProvider } from './KeyProvider'; @@ -48,11 +44,7 @@ function makeManager() { describe('E2EEManager log-level listener lifecycle', () => { const startingLevel = workerLogger.getLevel(); -<<<<<<< HEAD const startingCount = getWorkerLogLevelListenerCount(); -======= - const startingCount = _getWorkerLogLevelListenerCount(); ->>>>>>> 6c108c99 (add tests and reject pending futures on dispose) afterEach(() => { setLogLevel(startingLevel); @@ -78,11 +70,7 @@ describe('E2EEManager log-level listener lifecycle', () => { setLogLevel(LogLevel.warn); expect(worker.postMessage).not.toHaveBeenCalled(); -<<<<<<< HEAD expect(getWorkerLogLevelListenerCount()).toBe(startingCount); -======= - expect(_getWorkerLogLevelListenerCount()).toBe(startingCount); ->>>>>>> 6c108c99 (add tests and reject pending futures on dispose) }); it('re-setup with a new room does not stack listeners', () => { @@ -90,15 +78,9 @@ describe('E2EEManager log-level listener lifecycle', () => { const roomA = new Room(); const roomB = new Room(); manager.setup(roomA); -<<<<<<< HEAD const countAfterFirst = getWorkerLogLevelListenerCount(); manager.setup(roomB); expect(getWorkerLogLevelListenerCount()).toBe(countAfterFirst); -======= - const countAfterFirst = _getWorkerLogLevelListenerCount(); - manager.setup(roomB); - expect(_getWorkerLogLevelListenerCount()).toBe(countAfterFirst); ->>>>>>> 6c108c99 (add tests and reject pending futures on dispose) worker.postMessage.mockClear(); setLogLevel(LogLevel.debug); @@ -112,11 +94,7 @@ describe('E2EEManager log-level listener lifecycle', () => { manager.setup(room); manager.dispose(); manager.dispose(); -<<<<<<< HEAD expect(getWorkerLogLevelListenerCount()).toBe(startingCount); -======= - expect(_getWorkerLogLevelListenerCount()).toBe(startingCount); ->>>>>>> 6c108c99 (add tests and reject pending futures on dispose) }); it('dispose() rejects pending encrypt/decrypt futures and clears both maps', async () => { @@ -176,11 +154,7 @@ describe('E2EEManager GC cleanup', () => { it.skipIf(!(globalThis as any).gc)( 'releases the log-level listener when the manager is garbage collected', async () => { -<<<<<<< HEAD const before = getWorkerLogLevelListenerCount(); -======= - const before = _getWorkerLogLevelListenerCount(); ->>>>>>> 6c108c99 (add tests and reject pending futures on dispose) // Construct + subscribe in an IIFE so nothing lives on the test's stack. // Direct call to the private subscription — no Room, no leaky graph. @@ -194,11 +168,7 @@ describe('E2EEManager GC cleanup', () => { false, ); (manager as unknown as { subscribeToLogLevelChanges(): void }).subscribeToLogLevelChanges(); -<<<<<<< HEAD expect(getWorkerLogLevelListenerCount()).toBe(before + 1); -======= - expect(_getWorkerLogLevelListenerCount()).toBe(before + 1); ->>>>>>> 6c108c99 (add tests and reject pending futures on dispose) return new WeakRef(manager); })(); @@ -211,27 +181,16 @@ describe('E2EEManager GC cleanup', () => { // Read the listener count (which does not touch the referent) instead. const gc = (globalThis as any).gc as (opts?: { type?: 'major'; execution?: 'sync' }) => void; for (let i = 0; i < 50; i++) { -<<<<<<< HEAD // eslint-disable-next-line no-void void new Array(100_000).fill({ i }); gc({ type: 'major', execution: 'sync' }); await new Promise((r) => setImmediate(r)); if (getWorkerLogLevelListenerCount() === before) break; -======= - void new Array(100_000).fill({ i }); - gc({ type: 'major', execution: 'sync' }); - await new Promise((r) => setImmediate(r)); - if (_getWorkerLogLevelListenerCount() === before) break; ->>>>>>> 6c108c99 (add tests and reject pending futures on dispose) } // Diagnostic: separate "manager wasn't collected" from "FR didn't fire". expect(managerRef.deref(), 'manager was not collected — strong ref leaked').toBeUndefined(); -<<<<<<< HEAD expect(getWorkerLogLevelListenerCount()).toBe(before); -======= - expect(_getWorkerLogLevelListenerCount()).toBe(before); ->>>>>>> 6c108c99 (add tests and reject pending futures on dispose) }, ); });