snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
254
packages/sync-core/src/lib/MicrotaskNotifier.test.ts
Normal file
254
packages/sync-core/src/lib/MicrotaskNotifier.test.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { vi } from 'vitest'
|
||||
import { MicrotaskNotifier } from './MicrotaskNotifier'
|
||||
|
||||
// Helper to flush all pending microtasks
|
||||
async function flushMicrotasks() {
|
||||
await Promise.resolve()
|
||||
}
|
||||
|
||||
describe('MicrotaskNotifier', () => {
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
describe('deferred delivery (MN1)', () => {
|
||||
it('[MN1] does not call listeners synchronously', () => {
|
||||
const notifier = new MicrotaskNotifier<[string]>()
|
||||
const listener = vi.fn()
|
||||
|
||||
notifier.register(listener)
|
||||
notifier.notify('hello')
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('[MN1] delivers the notification arguments to every listener on a microtask', async () => {
|
||||
const notifier = new MicrotaskNotifier<[string, number, boolean]>()
|
||||
const listener1 = vi.fn()
|
||||
const listener2 = vi.fn()
|
||||
|
||||
notifier.register(listener1)
|
||||
notifier.register(listener2)
|
||||
await flushMicrotasks()
|
||||
|
||||
notifier.notify('test', 123, true)
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(listener1).toHaveBeenCalledExactlyOnceWith('test', 123, true)
|
||||
expect(listener2).toHaveBeenCalledExactlyOnceWith('test', 123, true)
|
||||
})
|
||||
|
||||
it('[MN1] delivers multiple notifications in order', async () => {
|
||||
const notifier = new MicrotaskNotifier<[number]>()
|
||||
const calls: number[] = []
|
||||
const listener = vi.fn((n: number) => calls.push(n))
|
||||
|
||||
notifier.register(listener)
|
||||
await flushMicrotasks()
|
||||
|
||||
notifier.notify(1)
|
||||
notifier.notify(2)
|
||||
notifier.notify(3)
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(calls).toEqual([1, 2, 3])
|
||||
})
|
||||
|
||||
it('[MN1] notify with no listeners does not throw', async () => {
|
||||
const notifier = new MicrotaskNotifier<[string]>()
|
||||
|
||||
expect(() => notifier.notify('test')).not.toThrow()
|
||||
await flushMicrotasks()
|
||||
})
|
||||
})
|
||||
|
||||
describe('deferred registration (MN2)', () => {
|
||||
it('[MN2] a listener misses notifications issued before register was called', async () => {
|
||||
const notifier = new MicrotaskNotifier<[string]>()
|
||||
const listener = vi.fn()
|
||||
|
||||
notifier.notify('before registration')
|
||||
notifier.register(listener)
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('[MN2] a listener receives notifications issued after register in the same synchronous block', async () => {
|
||||
const notifier = new MicrotaskNotifier<[string]>()
|
||||
const listener = vi.fn()
|
||||
|
||||
// Both register and notify queue microtasks; microtasks run FIFO, so
|
||||
// the add runs first and the notification is delivered
|
||||
notifier.register(listener)
|
||||
notifier.notify('same sync block')
|
||||
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(listener).toHaveBeenCalledExactlyOnceWith('same sync block')
|
||||
})
|
||||
|
||||
it('[MN2] registering during a notification defers the new listener to later notifications', async () => {
|
||||
const notifier = new MicrotaskNotifier<[string]>()
|
||||
const lateListener = vi.fn()
|
||||
const listener1 = vi.fn(() => {
|
||||
notifier.register(lateListener)
|
||||
})
|
||||
|
||||
notifier.register(listener1)
|
||||
await flushMicrotasks()
|
||||
|
||||
notifier.notify('first')
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(lateListener).not.toHaveBeenCalled()
|
||||
|
||||
await flushMicrotasks() // late registration completes
|
||||
|
||||
notifier.notify('second')
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(lateListener).toHaveBeenCalledExactlyOnceWith('second')
|
||||
})
|
||||
})
|
||||
|
||||
describe('unsubscribing (MN3)', () => {
|
||||
it('[MN3] unsubscribe is synchronous and stops future notifications', async () => {
|
||||
const notifier = new MicrotaskNotifier<[string]>()
|
||||
const listener = vi.fn()
|
||||
const other = vi.fn()
|
||||
|
||||
const unsubscribe = notifier.register(listener)
|
||||
notifier.register(other)
|
||||
await flushMicrotasks()
|
||||
|
||||
unsubscribe()
|
||||
notifier.notify('should not receive')
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
// other listeners are unaffected
|
||||
expect(other).toHaveBeenCalledExactlyOnceWith('should not receive')
|
||||
})
|
||||
|
||||
it('[MN3] unsubscribing before the add microtask runs prevents the registration entirely', async () => {
|
||||
const notifier = new MicrotaskNotifier<[string]>()
|
||||
const listener = vi.fn()
|
||||
|
||||
const unsubscribe = notifier.register(listener)
|
||||
unsubscribe() // before the add microtask runs
|
||||
|
||||
await flushMicrotasks()
|
||||
|
||||
notifier.notify('should not receive')
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('[MN3] double unsubscribe is safe', async () => {
|
||||
const notifier = new MicrotaskNotifier<[string]>()
|
||||
const listener = vi.fn()
|
||||
|
||||
const unsubscribe = notifier.register(listener)
|
||||
await flushMicrotasks()
|
||||
|
||||
unsubscribe()
|
||||
unsubscribe()
|
||||
unsubscribe()
|
||||
|
||||
notifier.notify('test')
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('[MN3] registering the same function twice', async () => {
|
||||
const notifier = new MicrotaskNotifier<[string]>()
|
||||
const listener = vi.fn()
|
||||
|
||||
const unsub1 = notifier.register(listener)
|
||||
const _unsub2 = notifier.register(listener)
|
||||
await flushMicrotasks()
|
||||
|
||||
notifier.notify('test')
|
||||
await flushMicrotasks()
|
||||
|
||||
// The listener set deduplicates the same function reference, so it is
|
||||
// only called once per notification
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Unsubscribing via the first registration removes the function for
|
||||
// both registrations
|
||||
unsub1()
|
||||
notifier.notify('test2')
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('[MN3] unsubscribing during notification iteration takes effect for later notifications', async () => {
|
||||
const notifier = new MicrotaskNotifier<[string]>()
|
||||
let unsub2: (() => void) | undefined = undefined
|
||||
|
||||
const listener1 = vi.fn(() => {
|
||||
// Unsubscribe listener2 while iteration is in progress
|
||||
unsub2?.()
|
||||
})
|
||||
const listener2 = vi.fn()
|
||||
const listener3 = vi.fn()
|
||||
|
||||
notifier.register(listener1)
|
||||
unsub2 = notifier.register(listener2)
|
||||
notifier.register(listener3)
|
||||
await flushMicrotasks()
|
||||
|
||||
notifier.notify('test')
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(listener1).toHaveBeenCalled()
|
||||
expect(listener3).toHaveBeenCalled()
|
||||
|
||||
listener1.mockClear()
|
||||
listener2.mockClear()
|
||||
listener3.mockClear()
|
||||
|
||||
notifier.notify('test2')
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(listener1).toHaveBeenCalled()
|
||||
expect(listener2).not.toHaveBeenCalled()
|
||||
expect(listener3).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('listener errors (MN4)', () => {
|
||||
it('[MN4] a listener that throws is caught and logged, and remaining listeners still run', async () => {
|
||||
const notifier = new MicrotaskNotifier<[string]>()
|
||||
const errorListener = vi.fn(() => {
|
||||
throw new Error('listener error')
|
||||
})
|
||||
const normalListener = vi.fn()
|
||||
|
||||
notifier.register(errorListener)
|
||||
notifier.register(normalListener)
|
||||
await flushMicrotasks()
|
||||
|
||||
notifier.notify('test')
|
||||
await flushMicrotasks()
|
||||
|
||||
expect(errorListener).toHaveBeenCalled()
|
||||
expect(normalListener).toHaveBeenCalled()
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error in MicrotaskNotifier listener',
|
||||
expect.any(Error)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user