local utils = require('promise-async.utils') local promiseId = {'promise-async'} local errFactory = require('promise-async.error') local shortSrc = debug.getinfo(1, 'S').short_src ---@alias PromiseState ---| 1 #PENDING ---| 2 #FULFILLED ---| 3 #REJECTED local PENDING = 1 local FULFILLED = 2 local REJECTED = 3 -- ---@class Promise ---@field state PromiseState ---@field result any ---@field queue table ---@field needHandleRejection? boolean ---@field err? PromiseAsyncError local Promise = setmetatable({_id = promiseId}, { __call = function(self, executor) return self:new(executor) end }) Promise.__index = Promise local function loadEventLoop() local success, res = pcall(require, 'promise-async.loop') assert(success, 'Promise need an EventLoop, ' .. 'luv module or a customized EventLoop module is expected.') return res end if vim then -- `require` in Neovim is hacked by its get_runtime API, may throw an error while calling -- `require` in libuv, require at once as a workaround. Promise.loop = require('promise-async.loop') else Promise.loop = setmetatable({}, { __index = function(_, key) local loop = loadEventLoop() rawset(Promise, 'loop', loop) return loop[key] end, __newindex = function(_, key, value) local loop = loadEventLoop() rawset(Promise, 'loop', loop) Promise.loop[key] = value end }) end function Promise:__tostring() local state = self.state if state == PENDING then return 'Promise { }' elseif state == REJECTED then return ('Promise { %s }'):format(tostring(self.result)) else return ('Promise { %s }'):format(tostring(self.result)) end end local function noop() end ---@param o any ---@param typ? string ---@return boolean function Promise.isInstance(o, typ) return (typ or type(o)) == 'table' and o._id == promiseId end ---must get `thenCall` field from `o` at one time, can't call repeatedly. ---@param o any ---@param typ? type ---@return function? local function getThenable(o, typ) local thenCall if (typ or type(o)) == 'table' then thenCall = o.thenCall if type(thenCall) ~= 'function' then thenCall = nil end end return thenCall end ---@param err any ---@return PromiseAsyncError local function buildError(err) local o = errFactory.new(err) local level = 4 local value local thread = coroutine.running() repeat value = errFactory.format(thread, level, shortSrc) level = level + 1 o:push(value) until not value table.remove(o.queue) return o end local resolvePromise, rejectPromise ---@param promise Promise local function handleQueue(promise) local queue = promise.queue if #queue == 0 then return end if promise.needHandleRejection and #queue > 0 then promise.needHandleRejection = nil end promise.queue = {} Promise.loop.nextTick(function() local state, result = promise.state, promise.result for _, q in ipairs(queue) do local newPromise, onFulfilled, onRejected = q[1], q[2], q[3] local func if state == FULFILLED then if utils.getCallable(onFulfilled) then func = onFulfilled else resolvePromise(newPromise, result) end elseif state == REJECTED then if utils.getCallable(onRejected) then func = onRejected else rejectPromise(newPromise, result) end end if func then local ok, res = xpcall(function() return func(result) end, function(errmsg) if type(errmsg) == 'string' then newPromise.err = buildError(errmsg) end return errmsg end) if ok then resolvePromise(newPromise, res) else rejectPromise(newPromise, res) end end end end) end ---@param promise Promise ---@param result any ---@param state PromiseState local function transition(promise, result, state) if promise.state ~= PENDING then return end promise.result = result promise.state = state handleQueue(promise) end ---@param promise Promise ---@param executor PromiseExecutor ---@param self? table local function wrapExecutor(promise, executor, self) local called = false local resolve = function(value) if called then return end resolvePromise(promise, value) called = true end local reject = function(reason) if called then return end rejectPromise(promise, reason) called = true end local ok, res = xpcall(function() if self then ---@diagnostic disable-next-line: redundant-parameter, param-type-mismatch return executor(self, resolve, reject) else return executor(resolve, reject) end end, function(errmsg) if type(errmsg) == 'string' then promise.err = buildError(errmsg) end return errmsg end) if not ok and not called then reject(res) end end ---@param promise Promise local function handleRejection(promise) promise.needHandleRejection = true Promise.loop.nextIdle(function() if promise.needHandleRejection then promise.needHandleRejection = nil local err = promise.err if not err then err = errFactory.new(promise.result) end err:unshift('UnhandledPromiseRejection with the reason:') error(err) end end) end ---@param promise Promise ---@param reason any rejectPromise = function(promise, reason) handleRejection(promise) transition(promise, reason, REJECTED) end ---@param promise Promise ---@param value any resolvePromise = function(promise, value) if promise == value then local reason = debug.traceback('TypeError: Chaining cycle detected for promise') rejectPromise(promise, reason) return end local valueType = type(value) if Promise.isInstance(value, valueType) then value:thenCall(function(val) resolvePromise(promise, val) end, function(reason) rejectPromise(promise, reason) end) else local thenCall = getThenable(value, valueType) if thenCall then wrapExecutor(promise, thenCall, value) else transition(promise, value, FULFILLED) end end end function Promise:new(executor) utils.assertType(executor, 'function') local o = self == Promise and setmetatable({}, self) or self o.state = PENDING o.result = nil o.queue = {} o.needHandleRejection = nil if executor ~= noop then wrapExecutor(o, executor) end return o end function Promise:thenCall(onFulfilled, onRejected) local o = self.new(Promise, noop) table.insert(self.queue, {o, onFulfilled, onRejected}) if self.state ~= PENDING then handleQueue(self) end return o end function Promise:catch(onRejected) return self:thenCall(nil, onRejected) end function Promise:finally(onFinally) local function wrapFinally() if utils.getCallable(onFinally) then ---@diagnostic disable-next-line: need-check-nil onFinally() end end return self:thenCall(function(value) wrapFinally() return value end, function(reason) wrapFinally() return Promise.reject(reason) end) end function Promise.resolve(value) local typ = type(value) if Promise.isInstance(value, typ) then return value else local o = Promise:new(noop) local thenCall = getThenable(value, typ) if thenCall then wrapExecutor(o, thenCall, value) else o.state = FULFILLED o.result = value end return o end end function Promise.reject(reason) local o = Promise:new(noop) o.state = REJECTED o.result = reason handleRejection(o) return o end function Promise.all(values) utils.assertType(values, 'table') return Promise:new(function(resolve, reject) local res = {} local cnt = 0 for k, v in pairs(values) do cnt = cnt + 1 Promise.resolve(v):thenCall(function(value) res[k] = value cnt = cnt - 1 if cnt == 0 then resolve(res) end end, function(reason) reject(reason) end) end if cnt == 0 then resolve(res) end end) end function Promise.allSettled(values) utils.assertType(values, 'table') return Promise:new(function(resolve, reject) local res = {} local cnt = 0 local _ = reject for k, v in pairs(values) do cnt = cnt + 1 Promise.resolve(v):thenCall(function(value) res[k] = {status = 'fulfilled', value = value} end, function(reason) res[k] = {status = 'rejected', reason = reason} end):finally(function() cnt = cnt - 1 if cnt == 0 then resolve(res) end end) end if cnt == 0 then resolve(res) end end) end function Promise.any(values) utils.assertType(values, 'table') return Promise:new(function(resolve, reject) local cnt = 0 local function rejectAggregateError() if cnt == 0 then reject('AggregateError: All promises were rejected') end end for _, p in pairs(values) do cnt = cnt + 1 Promise.resolve(p):thenCall(function(value) resolve(value) end, function() end):finally(function() cnt = cnt - 1 rejectAggregateError() end) end rejectAggregateError() end) end function Promise.race(values) utils.assertType(values, 'table') return Promise:new(function(resolve, reject) for _, p in pairs(values) do Promise.resolve(p):thenCall(function(value) resolve(value) end, function(reason) reject(reason) end) end end) end return Promise