util/promise.lua

changeset 0
550f506de75a
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/promise.lua	Thu Dec 03 17:05:27 2020 +0000
@@ -0,0 +1,177 @@
+local promise_methods = {};
+local promise_mt = { __name = "promise", __index = promise_methods };
+
+local xpcall = require "util.xpcall".xpcall;
+
+function promise_mt:__tostring()
+	return  "promise (" .. (self._state or "invalid") .. ")";
+end
+
+local function is_promise(o)
+	local mt = getmetatable(o);
+	return mt == promise_mt;
+end
+
+local function wrap_handler(f, resolve, reject, default)
+	if not f then
+		return default;
+	end
+	return function (param)
+		local ok, ret = xpcall(f, debug.traceback, param);
+		if ok then
+			resolve(ret);
+		else
+			reject(ret);
+		end
+		return true;
+	end;
+end
+
+local function next_pending(self, on_fulfilled, on_rejected, resolve, reject)
+	table.insert(self._pending_on_fulfilled, wrap_handler(on_fulfilled, resolve, reject, resolve));
+	table.insert(self._pending_on_rejected, wrap_handler(on_rejected, resolve, reject, reject));
+end
+
+local function next_fulfilled(promise, on_fulfilled, on_rejected, resolve, reject) -- luacheck: ignore 212/on_rejected
+	wrap_handler(on_fulfilled, resolve, reject, resolve)(promise.value);
+end
+
+local function next_rejected(promise, on_fulfilled, on_rejected, resolve, reject) -- luacheck: ignore 212/on_fulfilled
+	wrap_handler(on_rejected, resolve, reject, reject)(promise.reason);
+end
+
+local function promise_settle(promise, new_state, new_next, cbs, value)
+	if promise._state ~= "pending" then
+		return;
+	end
+	promise._state = new_state;
+	promise._next = new_next;
+	for _, cb in ipairs(cbs) do
+		cb(value);
+	end
+	-- No need to keep references to callbacks
+	promise._pending_on_fulfilled = nil;
+	promise._pending_on_rejected = nil;
+	return true;
+end
+
+local function new_resolve_functions(p)
+	local resolved = false;
+	local function _resolve(v)
+		if resolved then return; end
+		resolved = true;
+		if is_promise(v) then
+			v:next(new_resolve_functions(p));
+		elseif promise_settle(p, "fulfilled", next_fulfilled, p._pending_on_fulfilled, v) then
+			p.value = v;
+		end
+
+	end
+	local function _reject(e)
+		if resolved then return; end
+		resolved = true;
+		if promise_settle(p, "rejected", next_rejected, p._pending_on_rejected, e) then
+			p.reason = e;
+		end
+	end
+	return _resolve, _reject;
+end
+
+local function new(f)
+	local p = setmetatable({ _state = "pending", _next = next_pending, _pending_on_fulfilled = {}, _pending_on_rejected = {} }, promise_mt);
+	if f then
+		local resolve, reject = new_resolve_functions(p);
+		local ok, ret = xpcall(f, debug.traceback, resolve, reject);
+		if not ok and p._state == "pending" then
+			reject(ret);
+		end
+	end
+	return p;
+end
+
+local function all(promises)
+	return new(function (resolve, reject)
+		local count, total, results = 0, #promises, {};
+		for i = 1, total do
+			promises[i]:next(function (v)
+				results[i] = v;
+				count = count + 1;
+				if count == total then
+					resolve(results);
+				end
+			end, reject);
+		end
+	end);
+end
+
+local function all_settled(promises)
+	return new(function (resolve)
+		local count, total, results = 0, #promises, {};
+		for i = 1, total do
+			promises[i]:next(function (v)
+				results[i] = { status = "fulfilled", value = v };
+				count = count + 1;
+				if count == total then
+					resolve(results);
+				end
+			end, function (e)
+				results[i] = { status = "rejected", reason = e };
+				count = count + 1;
+				if count == total then
+					resolve(results);
+				end
+			end);
+		end
+	end);
+end
+
+local function race(promises)
+	return new(function (resolve, reject)
+		for i = 1, #promises do
+			promises[i]:next(resolve, reject);
+		end
+	end);
+end
+
+local function resolve(v)
+	return new(function (_resolve)
+		_resolve(v);
+	end);
+end
+
+local function reject(v)
+	return new(function (_, _reject)
+		_reject(v);
+	end);
+end
+
+local function try(f)
+	return resolve():next(function () return f(); end);
+end
+
+function promise_methods:next(on_fulfilled, on_rejected)
+	return new(function (resolve, reject) --luacheck: ignore 431/resolve 431/reject
+		self:_next(on_fulfilled, on_rejected, resolve, reject);
+	end);
+end
+
+function promise_methods:catch(on_rejected)
+	return self:next(nil, on_rejected);
+end
+
+function promise_methods:finally(on_finally)
+	local function _on_finally(value) on_finally(); return value; end
+	local function _on_catch_finally(err) on_finally(); return reject(err); end
+	return self:next(_on_finally, _on_catch_finally);
+end
+
+return {
+	new = new;
+	resolve = resolve;
+	reject = reject;
+	all = all;
+	all_settled = all_settled;
+	race = race;
+	try = try;
+	is_promise = is_promise;
+}

mercurial