diff --git a/src/events/onEvent/index.test.ts b/src/events/onEvent/index.test.ts new file mode 100644 index 0000000..2665504 --- /dev/null +++ b/src/events/onEvent/index.test.ts @@ -0,0 +1,81 @@ +/* global describe, it */ + +// import { testLog } from '../emitLog' + +import { Event, makeEvent } from '.' + +describe('makeEvent', function () { + it('creates a simple event', function () { + const log = makeAssertLog() + const [on, emit]: Event = makeEvent() + + const unsubscribe = on(log) + emit('a') + emit('b') + log.assert('a', 'b') + + // Unsubscribe should work: + unsubscribe() + emit('c') + log.assert() + }) + + it('unsubscribe is unique & idempotent', function () { + const log = makeAssertLog() + const [on, emit]: Event = makeEvent() + + // Subscribing twice gives double events: + const unsubscribe1 = on(log) + const unsubscribe2 = on(log) + emit('a') + log.assert('a', 'a') + + // The first unsubscribe should only remove the first callback: + unsubscribe1() + unsubscribe1() + emit('b') + log.assert('b') + + // Now everything should be unsubscribed: + unsubscribe2() + emit('c') + log.assert() + }) + + it('unsubscribe works during emit', function () { + const log = makeAssertLog() + const [on, emit]: Event = makeEvent() + + // Auto-unsubscribe to get a single event: + const unsubscribe = on(message => { + log(message) + unsubscribe() + }) + emit('a') + log.assert('a') + + // Future events should not show up: + emit('b') + log.assert() + }) + + it('double-emits do not crash', function () { + const log = makeAssertLog() + const [on, emit]: Event = makeEvent() + + const unsubscribe = on(message => { + log('first', message) + + // Completely change the subscribers: + unsubscribe() + on(message => log('second', message)) + + // Do a recursive emit: + emit('b') + }) + + // Kick off the process: + emit('a') + log.assert('first a', 'second b') + }) +}) diff --git a/src/events/onEvent/index.ts b/src/events/onEvent/index.ts new file mode 100644 index 0000000..0e8215b --- /dev/null +++ b/src/events/onEvent/index.ts @@ -0,0 +1,89 @@ +// @export Callback +export type Callback = (payload: T) => void +// @export Unsubscribe +export type Unsubscribe = () => void + +// Standalone events: +// @export OnEvent +export type OnEvent = (callback: Callback) => Unsubscribe +export type EmitEvent = (payload: T) => void +export type Event = [OnEvent, EmitEvent] + + +/** + * Named events + * + */ +export type OnEvents = ( + name: Name, + callback: Callback +) => Unsubscribe +export type EmitEvents = ( + name: Name, + payload: T[Name] +) => void +export type Events = [OnEvents, EmitEvents] + +/** + * Create a standalone event. + * Returns a subscriber function and an emitter function. + */ +export function makeEvent(): Event { + let callbacks: Array> = [] + let callbacksLocked = false + + // Clone the callback list if necessary, + // so the changes will only apply on the next emit. + function unlockCallbacks(): void { + if (callbacksLocked) { + callbacksLocked = false + callbacks = callbacks.slice() + } + } + + const on: OnEvent = callback => { + unlockCallbacks() + callbacks.push(callback) + + let subscribed = true + return function unsubscribe() { + if (subscribed) { + subscribed = false + unlockCallbacks() + callbacks.splice(callbacks.indexOf(callback), 1) + } + } + } + + const emit: EmitEvent = payload => { + callbacksLocked = true + const snapshot = callbacks + for (let i = 0; i < snapshot.length; ++i) { + snapshot[i](payload) + } + callbacksLocked = false + } + + return [on, emit] +} + +type EventMap = { + [Name in keyof T]?: Event +} + +export function makeEvents(): Events { + const events: EventMap = {} + + const on: OnEvents = (name, callback) => { + let event = events[name] + if (event == null) event = events[name] = makeEvent() + return event[0](callback) + } + + const emit: EmitEvents = (name, payload) => { + const event = events[name] + if (event != null) event[1](payload) + } + + return [on, emit] +};