Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions src/essence/mmgisAPI/mmgisAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -1072,13 +1072,31 @@ var mmgisAPI = {
off: events.off,

/**
* Emit an event to all subscribers
* Emit an event to all subscribers. A handler that throws is reported and
* skipped, so one bad listener cannot abort the emit.
* @param {string} event - Event name
* @param {*} data - Event data to pass to subscribers
* @example
* mmgisAPI.emit('layer:toggle', { layerName: 'Terrain', visible: true });
*/
emit: events.emit,
emit: (event, data) => {
const run = (fn, args, label) => {
try {
fn(...args)
} catch (err) {
console.error(`[mmgisAPI] ${label} for "${event}" threw:`, err)
}
}
// Dispatching from events.all — mitt's published subscriber map —
// rather than through events.emit, so each listener can be isolated.
// It restates two of mitt's rules: specific listeners before
// wildcards, and wildcards called with (type, data). A mitt upgrade is
// a place to re-check both.
//
// Copy before iterating: a handler may subscribe or unsubscribe mid-emit.
;[...(events.all.get(event) || [])].forEach((fn) => run(fn, [data], 'listener'))
;[...(events.all.get('*') || [])].forEach((fn) => run(fn, [event, data], 'wildcard listener'))
},

// ============ REQUEST/RESPONSE API ============

Expand Down
113 changes: 113 additions & 0 deletions tests/unit/mmgisAPIBus.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { test, expect, vi, afterEach } from 'vitest'

// Viewer_ pulls in Photosphere/ModelViewer/PDFViewer, which are JSX written in
// .js files that vite's import-analysis can't parse. Nothing here needs the
// real viewers, so stub the aggregator to keep the import chain parseable.
vi.mock('../../src/essence/Basics/Viewer_/Viewer_', () => ({ default: {} }))

import { mmgisAPI } from '../../src/essence/mmgisAPI/mmgisAPI'

// The bus is a module-level singleton, so a subscription outliving its test is
// heard by the next one. Collected here so a test that fails partway through
// still unsubscribes.
const cleanups = []
const listen = (event, fn) => {
cleanups.push(mmgisAPI.on(event, fn))
}

afterEach(() => {
cleanups.splice(0).forEach((off) => off())
vi.restoreAllMocks()
})

test('a listener receives the emitted payload', () => {
const seen = []
listen('test:payload', (data) => seen.push(data))
mmgisAPI.emit('test:payload', { a: 1 })
expect(seen).toEqual([{ a: 1 }])
})

test('a throwing listener does not reach the emitter', () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
listen('test:isolation', () => { throw new Error('boom') })
expect(() => mmgisAPI.emit('test:isolation', { a: 1 })).not.toThrow()
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('test:isolation'),
expect.any(Error)
)
})

test('a throwing listener does not stop later listeners', () => {
vi.spyOn(console, 'error').mockImplementation(() => {})
const seen = []
listen('test:order', () => { throw new Error('boom') })
listen('test:order', () => { seen.push('second') })
mmgisAPI.emit('test:order', {})
expect(seen).toEqual(['second'])
})

test('wildcard listeners still receive event name and payload', () => {
const seen = []
listen('*', (type, data) => { seen.push([type, data]) })
mmgisAPI.emit('test:wildcard', { v: 7 })
expect(seen).toEqual([['test:wildcard', { v: 7 }]])
})

test('a throwing wildcard listener is isolated too', () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const seen = []
listen('*', () => { throw new Error('boom') })
listen('test:wildcard-isolation', () => seen.push('specific'))

expect(() => mmgisAPI.emit('test:wildcard-isolation', {})).not.toThrow()

expect(seen).toEqual(['specific'])
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('wildcard listener'),
expect.any(Error)
)
})

test('a listener that unsubscribes mid-emit does not disturb the rest', () => {
// Every listener registered when the emit began still receives it.
const seen = []
let offSecond
listen('test:mutation', () => seen.push('first'))
listen('test:mutation', () => {
seen.push('second')
offSecond()
})
offSecond = cleanups[cleanups.length - 1]
listen('test:mutation', () => seen.push('third'))

mmgisAPI.emit('test:mutation', {})
expect(seen).toEqual(['first', 'second', 'third'])

seen.length = 0
mmgisAPI.emit('test:mutation', {})
expect(seen).toEqual(['first', 'third'])
})

test('a listener that subscribes mid-emit is not called by that same emit', () => {
const seen = []
listen('test:late-subscribe', () => {
seen.push('first')
listen('test:late-subscribe', () => seen.push('added-during-emit'))
})

mmgisAPI.emit('test:late-subscribe', {})
expect(seen).toEqual(['first'])

seen.length = 0
mmgisAPI.emit('test:late-subscribe', {})
expect(seen).toEqual(['first', 'added-during-emit'])
})

test('specific listeners run before wildcards', () => {
const order = []
listen('*', () => order.push('wildcard'))
listen('test:precedence', () => order.push('specific'))

mmgisAPI.emit('test:precedence', {})
expect(order).toEqual(['specific', 'wildcard'])
})
Loading