Defold Event - Cross-Context Defold Event System

Hello! There are a bunch of new releases for Defold Event library.

What is new?

Queues

Queues let you store events and process them later in FIFO order. Unlike regular events that run callbacks immediately, queue events stay in the queue until a handler returns a non-nil value (to mark the event as handled).

Use instance-based queues with queue.create() or global queues via the queues module and a string id. Push from anywhere. When you subscribe a handler, it is called for each pending and future events in the queue. You can also process events manually with process() or process_next() without using subscribers. So you can use queues in a different way.

I’m using queues to store logic events to process them later in UI one by one if required. Or to communicate between GO and GUI context at init step sometimes.

Queues API

Example — UI events queue processing:

local queues = require("event.queues")

local function process_next_event(self)
	queues.process_next("quest_events", function(self, data)
		if data.type == "quest_started" then
			animate_quest_start(data)
		elseif data.type == "quest_completed" then
			animate_quest_complete(data)
		end

		return true
	end, self)
end

Example — Cross-context communication:

-- GUI context
local queues = require("event.queues")

function M:init(self)
	queues.push("get_atlas_path", {
		texture_name = gui.get_texture(self.node),
		sender = msg.url(),
	}, self._on_get_atlas_path, self)
end

function M:_on_get_atlas_path(atlas_path)
	local atlas_data = resource.get_atlas(atlas_path)
	local tex_info = resource.get_texture_info(atlas_data.texture)
end
-- GO script
---@param request druid.get_atlas_path_request
---@return string?
local function get_atlas_path(self, request)
	if not is_my_url(request.sender) then
		return nil
	end

	return go.get(request.sender, "textures", { key = request.texture_name })
end

function init(self)
	queues.subscribe("get_atlas_path", get_atlas_path, self)
end

function final(self)
	queues.unsubscribe("get_atlas_path", get_atlas_path, self)
end

Promises

The Promise module wraps asynchronous work and lets you chain with :next(), :catch(), and :finally(). Create a promise with promise.create(executor) or use promise.resolved(value) / promise.rejected(reason). Use promise.all() to wait for multiple promises and promise.race() for the first to finish. You can resolve or reject manually with :resolve() and :reject(), and build pipelines with :append() and :tail().

Promises API

Example — wrap function to promise:

local promise = require("event.promise")

---@param url string
---@return promise
local function load_data(url)
	local p = promise.create()

	http.request(url, "GET", function(response)
		if response.status == 200 then
			p:resolve(response.body)
		else
			p:reject(response.status)
		end
	end)

	return p
end

Example — build promise pipeline:

local promise = require("event.promise")

function M:load_next_level()
	return promise.resolved()
		:next(self.unload_level, nil, self)
		:next(self.wait_level_unload, nil, self)
		:next(levels.get_next_level, nil, self)
		:next(self.load_level, nil, self)
		:next(self.animate_level_appear, nil, self)
end

Event as callback

You can subscribe an event to another event: when the parent triggers, the child event is triggered too. Handy for forwarding or composing events. You can pass a callback context when subscribing an event, e.g. extra data that will be passed as the first argument when the child event runs (e.g. event_2:subscribe(event_1, "extra data")).

Example — forward one event to another:

local event = require("event.event")

local on_click = event.create(function(self) print("clicked!") end)
local on_ui_action = event.create()
on_ui_action:subscribe(on_click)  -- any trigger of on_ui_action also triggers on_click
on_ui_action:trigger()

Example — event with context:

local event_1 = event.create(function(self, x, y) print(self, x, y) end)
local event_2 = event.create()
event_2:subscribe(event_1, "my_context")  -- event_1 runs with "my_context" as self
event_2:trigger(10, 20)  -- prints "my_context", 10, 20

Subscribe Once

Subscribe a handler so it runs only once: after the first trigger (or first handled queue event), it is automatically unsubscribed. Available on events and queues: event:subscribe_once(), events.subscribe_once(), queue:subscribe_once(), and queues.subscribe_once(). Perfect for one-shot reactions like “when loaded”, “first click”, or “handle next item only”.

Example — event, one-time:

local event = require("event.event")
local on_loaded = event.create()

on_loaded:subscribe_once(function(self) print("Loaded once!") end, self)
on_loaded:trigger()  -- runs and unsubscribes
on_loaded:trigger()  -- no callback

Example — queue, handle one item then stop:

local queue = require("event.queue")
local task_queue = queue.create()

task_queue:subscribe_once(function(self, task)
	return run_task(task)  -- return non-nil to handle and auto-unsubscribe
end, self)
task_queue:push(task_a)
task_queue:push(task_b)  -- only first is handled by this subscriber

Event Modes

You can set how the event module runs callbacks and handles errors with event.set_mode(mode).

  • pcall (default): continue on error, not full tracebacks.
  • xpcall: continue on error, same idea but with full tracebacks (more memory).
  • none: stop on error, rethrow the error with full traceback.

Example:

local event = require("event.event")

-- Set inside game.project file or in your code:
event.set_mode("pcall")   -- safe default: log errors, continue
event.set_mode("xpcall")  -- full tracebacks, still continue
event.set_mode("none")    -- errors rethrown with full traceback

Full changes for 12-15 versions

V12

  • MIGRATION: Replace require("event.defer") with require("event.queues") in case of using defer module
  • BREAKING CHANGE: Refactored queue system to be instance-based like event system. queue.lua now creates queue instances with queue.create() instead of global system
  • Added queues.lua for global queues operations (renamed from old defer.lua functionality)
  • Added Promise module on top of event module
  • Fixed queue event processing order from LIFO to FIFO (events now processed in correct queue order)
  • Added event.set_mode function to set the event processing mode (pcall, xpcall and none)
  • The none mode to disable context changing in event callback and using pcall by default

V13

  • Added queue:process_next function to process exactly one event in the queue with a specific handler (subscribers will not be called)
  • Make promise:resolve and promise:reject public functions
  • Added promise:append function to append a task to the current promise without reassigning it.
  • Added promise:tail and promise:reset functions to manage the promise tail

V14

  • Enable cross-context for none event mode
  • In none mode, when an error occurs in a callback, the Lua error is rethrown with full traceback

V15

  • subscribe_once: Added event:subscribe_once, events.subscribe_once, queue:subscribe_once, and queues.subscribe_once to subscribe a handler for a single invocation. The handler is automatically unsubscribed after the first call.
  • Unsubscribe during trigger: Calling unsubscribe from inside a callback no longer breaks the current trigger iteration. Removals are applied after the trigger finishes, so all callbacks in the current trigger still run.
  • Fix when callback_context for subscription can’t be false
  • Event as callback: Support for callback_context when subscribing an event to another event (e.g. event_2:subscribe(event_1, "any additional data")).
  • Unsubscribe event from event: Fixed unsubscribing one event from another so the correct subscription is found and removed.
  • Various edge cases fixes and improvements.
  • Documentation and API pages updates.
11 Likes