Skip to content
Open
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
81 changes: 81 additions & 0 deletions src/events/onEvent/index.test.ts
Original file line number Diff line number Diff line change
@@ -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<string> = 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<string> = 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<string> = 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<string> = 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')
})
})
89 changes: 89 additions & 0 deletions src/events/onEvent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// @export Callback
export type Callback<T> = (payload: T) => void
// @export Unsubscribe
export type Unsubscribe = () => void

// Standalone events:
// @export OnEvent
export type OnEvent<T> = (callback: Callback<T>) => Unsubscribe
export type EmitEvent<T> = (payload: T) => void
export type Event<T> = [OnEvent<T>, EmitEvent<T>]


/**
* Named events
*
*/
export type OnEvents<T> = <Name extends keyof T>(
name: Name,
callback: Callback<T[Name]>
) => Unsubscribe
export type EmitEvents<T> = <Name extends keyof T>(
name: Name,
payload: T[Name]
) => void
export type Events<T> = [OnEvents<T>, EmitEvents<T>]

/**
* Create a standalone event.
* Returns a subscriber function and an emitter function.
*/
export function makeEvent<T>(): Event<T> {
let callbacks: Array<Callback<T>> = []
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<T> = 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<T> = payload => {
callbacksLocked = true
const snapshot = callbacks
for (let i = 0; i < snapshot.length; ++i) {
snapshot[i](payload)
}
callbacksLocked = false
}

return [on, emit]
}

type EventMap<T> = {
[Name in keyof T]?: Event<T[Name]>
}

export function makeEvents<T>(): Events<T> {
const events: EventMap<T> = {}

const on: OnEvents<T> = (name, callback) => {
let event = events[name]
if (event == null) event = events[name] = makeEvent()
return event[0](callback)
}

const emit: EmitEvents<T> = (name, payload) => {
const event = events[name]
if (event != null) event[1](payload)
}

return [on, emit]
};