File

util/promise.lua @ 13135:3fd24e1945b0

mod_storage_internal: Lazy-load archive items while iterating Very large list files previously ran into limits of the Lua parser, or just caused Prosody to freeze while parsing. Using the new index we can parse individual items one at a time. This probably won't reduce overall CPU usage, probably the opposite, but it will reduce the number of items in memory at once and allow collection of items after we iterated past them.
author Kim Alvefur <zash@zash.se>
date Wed, 12 May 2021 01:25:44 +0200
parent 12975:d10957394a3c
line wrap: on
line source

local promise_methods = {};
local promise_mt = { __name = "promise", __index = promise_methods };

local xpcall = require "prosody.util.xpcall".xpcall;
local unpack = table.unpack;

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 function _resolve(v)
		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 promise_settle(p, "rejected", next_rejected, p._pending_on_rejected, e) then
			p.reason = e;
		end
	end
	return _resolve, _reject;
end

local next_tick = function (f)
	f();
end

local function new(f)
	local p = setmetatable({ _state = "pending", _next = next_pending, _pending_on_fulfilled = {}, _pending_on_rejected = {} }, promise_mt);
	if f then
		next_tick(function()
			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);
	end
	return p;
end

local function all(promises)
	return new(function (resolve, reject)
		local settled, results, loop_finished = 0, {}, false;
		local total = 0;
		for k, v in pairs(promises) do
			if is_promise(v) then
				total = total + 1;
				v:next(function (value)
					results[k] = value;
					settled = settled + 1;
					if settled == total and loop_finished then
						resolve(results);
					end
				end, reject);
			else
				results[k] = v;
			end
		end
		loop_finished = true;
		if settled == total then
			resolve(results);
		end
	end);
end

local function all_settled(promises)
	return new(function (resolve)
		local settled, results, loop_finished = 0, {}, false;
		local total = 0;
		for k, v in pairs(promises) do
			if is_promise(v) then
				total = total + 1;
				v:next(function (value)
					results[k] = { status = "fulfilled", value = value };
					settled = settled + 1;
					if settled == total and loop_finished then
						resolve(results);
					end
				end, function (e)
					results[k] = { status = "rejected", reason = e };
					settled = settled + 1;
					if settled == total and loop_finished then
						resolve(results);
					end
				end);
			else
				results[k] = v;
			end
		end
		loop_finished = true;
		if settled == total then
			resolve(results);
		end
	end);
end

local function join(handler, ...)
	local promises, n = { ... }, select("#", ...);
	return all(promises):next(function (results)
		return handler(unpack(results, 1, n));
	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;
	join = join;
	reject = reject;
	all = all;
	all_settled = all_settled;
	race = race;
	try = try;
	is_promise = is_promise;
	set_nexttick = function(new_next_tick) next_tick = new_next_tick; end;
}