summaryrefslogtreecommitdiff
path: root/evloop.lua
blob: 637d26607bfa1aa1c19a50608e0b1f9ca85507a6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
-- module implementing nestable asynchronous event loop objects. the module
-- itself is an event loop object, representing the main event loop.

local M

local function new(...)
	local new = setmetatable({}, M)
	new.event_queue = {}
	new.tasks = {}
	for i = 1, select("#", ...) do
		new:wrap(select(i, ...))
	end
	return new
end

M = new()
M.__index = M

M.new = new

-- create a new task in the event loop.
function M:wrap(fn, ...)
	table.insert(self.event_queue, {coroutine.create(fn), ...})
end

-- send an event: evloop:queue(event, args...)
function M:queue(...)
	assert(type((...)))
	table.insert(self.event_queue, {...})
end

local function event_filter(timeout, ...)
	local filter = {}
	for i = 1, select("#", ...) do
		filter[select(i, ...)] = true
	end
	if type(timeout) == "string" then
		filter[timeout] = true
		timeout = nil
	end
	return filter, timeout
end

-- poll for an event: evloop.poll([timeout,] [events...])
-- returns: event, args...
function M.poll(...)
	local filter, timeout = event_filter(...)
	local e
	local t = 0
	repeat
		e = coroutine.yield(true)
		if e[1] == "timer" then
			t = t + e[2]
		end
		if timeout and t >= timeout then
			return
		end
	until filter[e[1]] or (not timeout and not next(filter))
	return unpack(e)
end

-- like evloop.poll but returns an iterator
function M.events(...)
	local filter, timeout = event_filter(...)
	local t = 0
	local function iter()
		if timeout and t >= timeout then
			t = t - timeout
			return false
		end
		local e = coroutine.yield(true)
		if e[1] == "timer" then
			t = t + e[2]
		end
		if (timeout or next(filter)) and not filter[e[1]] then
			return iter()
		else
			return unpack(e)
		end
	end
	return iter
end

function M.sleep(secs)
	M.poll(secs)
end

function M:quit(...)
	self:queue("quit", ...)
end

function M.debug_sleep(secs)
	local t = 0
	M.queue "debug.pause"
	while t < secs do
		local _, dt = M.poll "debug.timer"
		t = t + dt
	end
	M.queue "debug.unpause"
end

local function resume_task(co, ...)
	if coroutine.status(co) == "dead" then
		return false
	end

	local ok, ret = coroutine.resume(co, ...)
	if not ok then
		-- TODO: make this better somehow
		io.stderr:write(debug.traceback(co, ret)..'\n\n')
		error "error in event loop"
	end

	return coroutine.status(co) ~= "dead"
end

-- run each task in the event loop at once
function M:run()
	while true do
		::send_events::
		local q = self.event_queue
		self.event_queue = {}
		for _, e in ipairs(q) do
			if type(e[1]) == "thread" then
				-- start a new task
				if resume_task(e[1], unpack(e, 2)) then
					self.tasks[e[1]] = true
				end
			elseif e[1] == "quit" then
				return unpack(e, 2)
			else
				for task in pairs(self.tasks) do
					self.tasks[task] = resume_task(task, e) or nil
				end
			end
		end
		if #self.event_queue == 0 then
			if not next(self.tasks) then return end
			assert(coroutine.running(), "event queue depleted!")
			table.insert(self.event_queue, coroutine.yield(true))
		end
	end
end

return M