// the event emitter /* Bait is the event emitter for Hilbish. Much like Node.js and its `events` system, many actions in Hilbish emit events. Unlike Node.js, Hilbish events are global. So make sure to pick a unique name! Usage of the Bait module consists of userstanding event-driven architecture, but it's pretty simple: If you want to act on a certain event, you can `catch` it. You can act on events via callback functions. Examples of this are in the Hilbish default config! Consider this part of it: ```lua bait.catch('command.exit', function(code) running = false doPrompt(code ~= 0) doNotifyPrompt() end) ``` What this does is, whenever the `command.exit` event is thrown, this function will set the user prompt. */ package bait import ( "errors" "hilbish/util" rt "github.com/arnodel/golua/runtime" "github.com/arnodel/golua/lib/packagelib" ) type listenerType int const ( goListener listenerType = iota luaListener ) // Recoverer is a function which is called when a panic occurs in an event. type Recoverer func(event string, handler *Listener, err interface{}) // Listener is a struct that holds the handler for an event. type Listener struct{ typ listenerType once bool caller func(...interface{}) luaCaller *rt.Closure } type Bait struct{ Loader packagelib.Loader recoverer Recoverer handlers map[string][]*Listener rtm *rt.Runtime } // New creates a new Bait instance. func New(rtm *rt.Runtime) *Bait { b := &Bait{ handlers: make(map[string][]*Listener), rtm: rtm, } b.Loader = packagelib.Loader{ Load: b.loaderFunc, Name: "bait", } return b } // Emit throws an event. func (b *Bait) Emit(event string, args ...interface{}) { handles := b.handlers[event] if handles == nil { return } for idx, handle := range handles { defer func() { if err := recover(); err != nil { b.callRecoverer(event, handle, err) } }() if handle.typ == luaListener { funcVal := rt.FunctionValue(handle.luaCaller) var luaArgs []rt.Value for _, arg := range args { var luarg rt.Value switch arg.(type) { case rt.Value: luarg = arg.(rt.Value) default: luarg = rt.AsValue(arg) } luaArgs = append(luaArgs, luarg) } _, err := rt.Call1(b.rtm.MainThread(), funcVal, luaArgs...) if err != nil { if event != "error" { b.Emit("error", event, handle.luaCaller, err.Error()) return } // if there is an error in an error event handler, panic instead // (calls the go recoverer function) panic(err) } } else { handle.caller(args...) } if handle.once { b.removeListener(event, idx) } } } // On adds a Go function handler for an event. func (b *Bait) On(event string, handler func(...interface{})) *Listener { listener := &Listener{ typ: goListener, caller: handler, } b.addListener(event, listener) return listener } // OnLua adds a Lua function handler for an event. func (b *Bait) OnLua(event string, handler *rt.Closure) *Listener { listener :=&Listener{ typ: luaListener, luaCaller: handler, } b.addListener(event, listener) return listener } // Off removes a Go function handler for an event. func (b *Bait) Off(event string, listener *Listener) { handles := b.handlers[event] for i, handle := range handles { if handle == listener { b.removeListener(event, i) } } } // OffLua removes a Lua function handler for an event. func (b *Bait) OffLua(event string, handler *rt.Closure) { handles := b.handlers[event] for i, handle := range handles { if handle.luaCaller == handler { b.removeListener(event, i) } } } // Once adds a Go function listener for an event that only runs once. func (b *Bait) Once(event string, handler func(...interface{})) *Listener { listener := &Listener{ typ: goListener, once: true, caller: handler, } b.addListener(event, listener) return listener } // OnceLua adds a Lua function listener for an event that only runs once. func (b *Bait) OnceLua(event string, handler *rt.Closure) *Listener { listener := &Listener{ typ: luaListener, once: true, luaCaller: handler, } b.addListener(event, listener) return listener } // SetRecoverer sets the function to be executed when a panic occurs in an event. func (b *Bait) SetRecoverer(recoverer Recoverer) { b.recoverer = recoverer } func (b *Bait) addListener(event string, listener *Listener) { if b.handlers[event] == nil { b.handlers[event] = []*Listener{} } b.handlers[event] = append(b.handlers[event], listener) } func (b *Bait) removeListener(event string, idx int) { b.handlers[event][idx] = b.handlers[event][len(b.handlers[event]) - 1] b.handlers[event] = b.handlers[event][:len(b.handlers[event]) - 1] } func (b *Bait) callRecoverer(event string, handler *Listener, err interface{}) { if b.recoverer == nil { panic(err) } b.recoverer(event, handler, err) } func (b *Bait) loaderFunc(rtm *rt.Runtime) (rt.Value, func()) { exports := map[string]util.LuaExport{ "catch": util.LuaExport{b.bcatch, 2, false}, "catchOnce": util.LuaExport{b.bcatchOnce, 2, false}, "throw": util.LuaExport{b.bthrow, 1, true}, "release": util.LuaExport{b.brelease, 2, false}, "hooks": util.LuaExport{b.bhooks, 1, false}, } mod := rt.NewTable() util.SetExports(rtm, mod, exports) return rt.TableValue(mod), nil } func handleHook(t *rt.Thread, c *rt.GoCont, name string, catcher *rt.Closure, args ...interface{}) { funcVal := rt.FunctionValue(catcher) var luaArgs []rt.Value for _, arg := range args { var luarg rt.Value switch arg.(type) { case rt.Value: luarg = arg.(rt.Value) default: luarg = rt.AsValue(arg) } luaArgs = append(luaArgs, luarg) } _, err := rt.Call1(t, funcVal, luaArgs...) if err != nil { e := rt.NewError(rt.StringValue(err.Error())) e = e.AddContext(c.Next(), 1) // panicking here won't actually cause hilbish to panic and instead will // print the error and remove the hook (look at emission recover from above) panic(e) } } // catch(name, cb) // Catches an event. This function can be used to act on events. // #param name string The name of the hook. // #param cb function The function that will be called when the hook is thrown. /* #example bait.catch('hilbish.exit', function() print 'Goodbye Hilbish!' end) #example */ func (b *Bait) bcatch(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { name, catcher, err := util.HandleStrCallback(t, c) if err != nil { return nil, err } b.OnLua(name, catcher) return c.Next(), nil } // catchOnce(name, cb) // Catches an event, but only once. This will remove the hook immediately after it runs for the first time. // #param name string The name of the event // #param cb function The function that will be called when the event is thrown. func (b *Bait) bcatchOnce(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { name, catcher, err := util.HandleStrCallback(t, c) if err != nil { return nil, err } b.OnceLua(name, catcher) return c.Next(), nil } // hooks(name) -> table // Returns a table of functions that are hooked on an event with the corresponding `name`. // #param name string The name of the hook // #returns table<function> func (b *Bait) bhooks(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } evName, err := c.StringArg(0) if err != nil { return nil, err } noHooks := errors.New("no hooks for event " + evName) handlers := b.handlers[evName] if handlers == nil { return nil, noHooks } luaHandlers := rt.NewTable() for _, handler := range handlers { if handler.typ != luaListener { continue } luaHandlers.Set(rt.IntValue(luaHandlers.Len() + 1), rt.FunctionValue(handler.luaCaller)) } if luaHandlers.Len() == 0 { return nil, noHooks } return c.PushingNext1(t.Runtime, rt.TableValue(luaHandlers)), nil } // release(name, catcher) // Removes the `catcher` for the event with `name`. // For this to work, `catcher` has to be the same function used to catch // an event, like one saved to a variable. // #param name string Name of the event the hook is on // #param catcher function Hook function to remove /* #example local hookCallback = function() print 'hi' end bait.catch('event', hookCallback) -- a little while later.... bait.release('event', hookCallback) -- and now hookCallback will no longer be ran for the event. #example */ func (b *Bait) brelease(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { name, catcher, err := util.HandleStrCallback(t, c) if err != nil { return nil, err } b.OffLua(name, catcher) return c.Next(), nil } // throw(name, ...args) // #param name string The name of the hook. // #param args ...any The arguments to pass to the hook. // Throws a hook with `name` with the provided `args`. /* #example bait.throw('greeting', 'world') -- This can then be listened to via bait.catch('gretting', function(greetTo) print('Hello ' .. greetTo) end) #example */ func (b *Bait) bthrow(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { if err := c.Check1Arg(); err != nil { return nil, err } name, err := c.StringArg(0) if err != nil { return nil, err } ifaceSlice := make([]interface{}, len(c.Etc())) for i, v := range c.Etc() { ifaceSlice[i] = v } b.Emit(name, ifaceSlice...) return c.Next(), nil }