|
| 1 | +--- Lightweight Promise/A+ implementation for flattening callback chains. |
| 2 | +--- |
| 3 | +--- Converts nested callbacks into a linear chain: |
| 4 | +--- |
| 5 | +--- local P = require("poste.async.promise") |
| 6 | +--- |
| 7 | +--- P.new(function(resolve, reject) |
| 8 | +--- async_operation(function(result) |
| 9 | +--- resolve(result) |
| 10 | +--- end) |
| 11 | +--- end) |
| 12 | +--- :then_(function(result) |
| 13 | +--- return P.new(function(resolve) |
| 14 | +--- another_async_op(result, resolve) |
| 15 | +--- end) |
| 16 | +--- end) |
| 17 | +--- :then_(function(result) |
| 18 | +--- print("Final:", result) |
| 19 | +--- end) |
| 20 | +--- :catch_(function(err) |
| 21 | +--- vim.notify("Error: " .. err, vim.log.levels.ERROR) |
| 22 | +--- end) |
| 23 | + |
| 24 | +local M = {} |
| 25 | + |
| 26 | +local Promise = {} |
| 27 | +Promise.__index = Promise |
| 28 | + |
| 29 | +--- Create a new Promise. |
| 30 | +--- @param fn function(resolve, reject) Executor function |
| 31 | +--- @return Promise |
| 32 | +function M.new(fn) |
| 33 | + local self = setmetatable({ |
| 34 | + _state = "pending", -- "pending" | "fulfilled" | "rejected" |
| 35 | + _value = nil, |
| 36 | + _handlers = {}, |
| 37 | + }, Promise) |
| 38 | + |
| 39 | + local function resolve(value) |
| 40 | + if self._state ~= "pending" then return end |
| 41 | + self._state = "fulfilled" |
| 42 | + self._value = value |
| 43 | + self:_call_handlers() |
| 44 | + end |
| 45 | + |
| 46 | + local function reject(err) |
| 47 | + if self._state ~= "pending" then return end |
| 48 | + self._state = "rejected" |
| 49 | + self._value = err |
| 50 | + self:_call_handlers() |
| 51 | + end |
| 52 | + |
| 53 | + local ok, err = pcall(fn, resolve, reject) |
| 54 | + if not ok then |
| 55 | + reject(err) |
| 56 | + end |
| 57 | + |
| 58 | + return self |
| 59 | +end |
| 60 | + |
| 61 | +--- Register fulfillment handler. Returns a new Promise for chaining. |
| 62 | +--- @param on_fulfilled function(value) Called when promise is fulfilled |
| 63 | +--- @return Promise |
| 64 | +function Promise:then_(on_fulfilled) |
| 65 | + return M.new(function(resolve, reject) |
| 66 | + table.insert(self._handlers, { |
| 67 | + on_fulfilled = function(value) |
| 68 | + local ok, result = pcall(on_fulfilled, value) |
| 69 | + if ok then |
| 70 | + if type(result) == "table" and type(result.then_) == "function" then |
| 71 | + -- If the handler returns a Promise, chain it |
| 72 | + result:then_(resolve):catch_(reject) |
| 73 | + else |
| 74 | + resolve(result) |
| 75 | + end |
| 76 | + else |
| 77 | + reject(result) |
| 78 | + end |
| 79 | + end, |
| 80 | + on_rejected = function(err) |
| 81 | + reject(err) |
| 82 | + end, |
| 83 | + }) |
| 84 | + if self._state ~= "pending" then |
| 85 | + self:_call_handlers() |
| 86 | + end |
| 87 | + end) |
| 88 | +end |
| 89 | + |
| 90 | +--- Register rejection handler. |
| 91 | +--- @param on_rejected function(err) Called when promise is rejected |
| 92 | +--- @return Promise |
| 93 | +function Promise:catch_(on_rejected) |
| 94 | + return M.new(function(resolve, reject) |
| 95 | + table.insert(self._handlers, { |
| 96 | + on_fulfilled = function(value) |
| 97 | + resolve(value) |
| 98 | + end, |
| 99 | + on_rejected = function(err) |
| 100 | + local ok, result = pcall(on_rejected, err) |
| 101 | + if ok then |
| 102 | + resolve(result) -- recovery: treat as resolved |
| 103 | + else |
| 104 | + reject(result) |
| 105 | + end |
| 106 | + end, |
| 107 | + }) |
| 108 | + if self._state ~= "pending" then |
| 109 | + self:_call_handlers() |
| 110 | + end |
| 111 | + end) |
| 112 | +end |
| 113 | + |
| 114 | +--- Register a handler that runs regardless of fulfillment or rejection. |
| 115 | +--- @param fn function() |
| 116 | +--- @return Promise |
| 117 | +function Promise:finally_(fn) |
| 118 | + return self:then_(function(value) |
| 119 | + fn() |
| 120 | + return value |
| 121 | + end):catch_(function(err) |
| 122 | + fn() |
| 123 | + return M.reject(err) |
| 124 | + end) |
| 125 | +end |
| 126 | + |
| 127 | +--- Resolve immediately with a value. |
| 128 | +--- @param value any |
| 129 | +--- @return Promise |
| 130 | +function M.resolve(value) |
| 131 | + return M.new(function(resolve) resolve(value) end) |
| 132 | +end |
| 133 | + |
| 134 | +--- Reject immediately with an error. |
| 135 | +--- @param err any |
| 136 | +--- @return Promise |
| 137 | +function M.reject(err) |
| 138 | + return M.new(_, function() end, function(reject) reject(err) end) |
| 139 | +end |
| 140 | + |
| 141 | +--- Wait for all promises to settle. |
| 142 | +--- Returns Promise that resolves with array of values. |
| 143 | +--- @param promises Promise[] |
| 144 | +--- @return Promise |
| 145 | +function M.all(promises) |
| 146 | + return M.new(function(resolve, reject) |
| 147 | + if #promises == 0 then |
| 148 | + resolve({}) |
| 149 | + return |
| 150 | + end |
| 151 | + local results = {} |
| 152 | + local remaining = #promises |
| 153 | + for i, p in ipairs(promises) do |
| 154 | + p:then_(function(value) |
| 155 | + results[i] = value |
| 156 | + remaining = remaining - 1 |
| 157 | + if remaining == 0 then |
| 158 | + resolve(results) |
| 159 | + end |
| 160 | + end):catch_(function(err) |
| 161 | + reject(err) |
| 162 | + end) |
| 163 | + end |
| 164 | + end) |
| 165 | +end |
| 166 | + |
| 167 | +-- Internal: call pending handlers |
| 168 | +function Promise:_call_handlers() |
| 169 | + if self._state == "pending" then return end |
| 170 | + local handlers = self._handlers |
| 171 | + self._handlers = {} -- prevent re-entry |
| 172 | + for _, h in ipairs(handlers) do |
| 173 | + if self._state == "fulfilled" and h.on_fulfilled then |
| 174 | + h.on_fulfilled(self._value) |
| 175 | + elseif self._state == "rejected" and h.on_rejected then |
| 176 | + h.on_rejected(self._value) |
| 177 | + end |
| 178 | + end |
| 179 | +end |
| 180 | + |
| 181 | +return M |
0 commit comments