-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathpromise.lua
More file actions
421 lines (366 loc) · 10.4 KB
/
promise.lua
File metadata and controls
421 lines (366 loc) · 10.4 KB
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
---@generic T
---@generic U
---@class Promise<T>
---@field __index Promise<T>
---@field _resolved boolean
---@field _value T
---@field _error any
---@field _then_callbacks fun(value: T)[]
---@field _catch_callbacks fun(err: any)[]
---@field _coroutines thread[]
---@field new fun(): Promise<T>
---@field resolve fun(self: Promise<T>, value: T): Promise<T>
---@field reject fun(self: Promise<T>, err: any): Promise<T>
---@field and_then fun(self: Promise<T>, callback: fun(value: T): U | Promise<U> | nil): Promise<U>
---@field catch fun(self: Promise<T>, error_callback: fun(err: any): any | Promise<any> | nil): Promise<T>
---@field finally fun(self: Promise<T>, callback: fun(): nil): Promise<T>
---@field wait fun(self: Promise<T>, timeout?: integer, interval?: integer): T
---@field peek fun(self: Promise<T>): T
---@field is_resolved fun(self: Promise<T>): boolean
---@field is_rejected fun(self: Promise<T>): boolean
---@field await fun(self: Promise<T>): T
---@field is_promise fun(obj: any): boolean
---@field wrap fun(obj: T | Promise<T>): Promise<T>
---@field spawn fun(fn: fun(): T|nil): Promise<T>
---@field async fun(fn: fun(...): T?): fun(...): Promise<T>
---@field system fun(table, table): Promise<T>
local Promise = {}
Promise.__index = Promise
---Resume waiting coroutines with result
---@generic T
---@param coroutines thread[]
---@param value T
---@param err any
local function resume_coroutines(coroutines, value, err)
for _, co in ipairs(coroutines) do
vim.schedule(function()
if coroutine.status(co) == 'suspended' then
coroutine.resume(co, value, err)
end
end)
end
end
---Create a waitable promise that can be resolved or rejected later
---@return Promise<T>
function Promise.new()
local self = setmetatable({
_resolved = false,
_value = nil,
_error = nil,
_then_callbacks = {},
_catch_callbacks = {},
_coroutines = {},
}, Promise)
return self
end
---@param value T
---@return Promise<T>
function Promise:resolve(value)
if self._resolved then
return self
end
self._value = value
self._resolved = true
local schedule_then = vim.schedule_wrap(function(cb, v)
cb(v)
end)
for _, callback in ipairs(self._then_callbacks) do
schedule_then(callback, value)
end
resume_coroutines(self._coroutines, value, nil)
return self
end
---@param err any
---@return Promise<T>
function Promise:reject(err)
if self._resolved then
return self
end
self._error = err
self._resolved = true
local schedule_catch = vim.schedule_wrap(function(cb, e)
cb(e)
end)
for _, callback in ipairs(self._catch_callbacks) do
schedule_catch(callback, err)
end
resume_coroutines(self._coroutines, nil, err)
return self
end
---@generic U
---@param callback fun(value: T): U | Promise<U> | nil
---@return Promise<U>?
function Promise:and_then(callback)
if not callback then
error('callback is required')
end
local new_promise = Promise.new()
local handle_callback = function(value)
local ok, result = pcall(callback, value)
if not ok then
new_promise:reject(result)
return
end
if Promise.is_promise(result) then
result
:and_then(function(val)
new_promise:resolve(val)
end)
:catch(function(err)
new_promise:reject(err)
end)
else
new_promise:resolve(result)
end
end
if self._resolved and not self._error then
local schedule_then = vim.schedule_wrap(handle_callback)
schedule_then(self._value)
elseif self._resolved and self._error then
new_promise:reject(self._error)
else
table.insert(self._then_callbacks, handle_callback)
table.insert(self._catch_callbacks, function(err)
new_promise:reject(err)
end)
end
return new_promise
end
---@param error_callback fun(err: any): any | Promise<any> | nil
---@return Promise<T>
function Promise:catch(error_callback)
local new_promise = Promise.new()
local handle_error = function(err)
local ok, result = pcall(error_callback, err)
if not ok then
new_promise:reject(result)
return
end
if Promise.is_promise(result) then
result
:and_then(function(val)
new_promise:resolve(val)
end)
:catch(function(e)
new_promise:reject(e)
end)
else
new_promise:resolve(result)
end
end
local handle_success = function(value)
new_promise:resolve(value)
end
if self._resolved and self._error then
local schedule_catch = vim.schedule_wrap(handle_error)
schedule_catch(self._error)
elseif self._resolved and not self._error then
new_promise:resolve(self._value)
else
table.insert(self._catch_callbacks, handle_error)
table.insert(self._then_callbacks, handle_success)
end
return new_promise
end
---Execute a callback regardless of whether the promise resolves or rejects
---The callback is called without any arguments and its return value is ignored
---@param callback fun(): nil
---@return Promise<T>
function Promise:finally(callback)
local new_promise = Promise.new()
local handle_finally = function()
local ok, err = pcall(callback)
-- Ignore callback errors and result, finally doesn't change the promise chain
if not ok then
-- Log error but don't propagate it
vim.notify('Error in finally callback: ' .. tostring(err), vim.log.levels.WARN)
end
end
local handle_success = function(value)
handle_finally()
new_promise:resolve(value)
end
local handle_error = function(err)
handle_finally()
new_promise:reject(err)
end
if self._resolved and not self._error then
-- Promise already resolved successfully
local schedule_finally = vim.schedule_wrap(handle_success)
schedule_finally(self._value)
elseif self._resolved and self._error then
-- Promise already rejected
local schedule_finally = vim.schedule_wrap(handle_error)
schedule_finally(self._error)
else
-- Promise still pending, add callbacks
table.insert(self._then_callbacks, handle_success)
table.insert(self._catch_callbacks, handle_error)
end
return new_promise
end
--- Synchronously wait for the promise to resolve or reject
--- This will block the main thread, so use with caution
--- But is useful for synchronous code paths that need the result
---@generic T
---@param timeout integer|nil Timeout in milliseconds (default: 5000)
---@param interval integer|nil Interval in milliseconds to check (default: 20)
---@return T
function Promise:wait(timeout, interval)
if self._resolved then
if self._error then
error(self._error)
end
return self._value
end
timeout = timeout or 5000
interval = interval or 20
local success = vim.wait(timeout, function()
return self._resolved
end, interval)
if not success then
error('Promise timed out after ' .. timeout .. 'ms')
end
if self._error then
error(self._error)
end
return self._value
end
-- Tries to get the value without waiting
-- Useful for status checks where you don't want to block
---@generic T
---@return T
function Promise:peek()
return self._value
end
function Promise:is_resolved()
return self._resolved
end
function Promise:is_rejected()
return self._resolved and self._error ~= nil
end
---Await the promise from within a coroutine
---This function can only be called from within `coroutine.create` or `Promise.spawn` or `Promise.async`
---This will yield the coroutine until the promise resolves or rejects
---@generic T
---@return T
function Promise:await()
-- If already resolved, return immediately
local value
if self._resolved then
if self._error then
error(self._error)
end
value = self._value
---@cast value T
return value
end
-- Get the current coroutine
local co = coroutine.running()
if not co then
error('await() can only be called from within a coroutine')
end
table.insert(self._coroutines, co)
-- Yield and wait for resume
---@diagnostic disable-next-line: await-in-sync
local value, err = coroutine.yield()
if err then
error(err)
end
---@cast value T
return value
end
---@param obj any
---@return_cast obj Promise<T>
function Promise.is_promise(obj)
return type(obj) == 'table' and type(obj.and_then) == 'function' and type(obj.catch) == 'function'
end
---@param obj T | Promise<T>
---@return Promise<T>
---@return_cast T Promise<T>
function Promise.wrap(obj)
if Promise.is_promise(obj) then
return obj --[[@as Promise<T>]]
else
return Promise.new():resolve(obj)
end
end
---Run an async function in a coroutine
---The function can use promise:await() to wait for promises
---@generic T
---@param fn fun(): T
---@return Promise<T>
---@return_cast T Promise<T>
function Promise.spawn(fn)
local promise = Promise.new()
local co = coroutine.create(function()
local ok, result = pcall(fn)
if not ok then
promise:reject(result)
else
if Promise.is_promise(result) then
result
:and_then(function(val)
promise:resolve(val)
end)
:catch(function(err)
promise:reject(err)
end)
else
promise:resolve(result)
end
end
end)
local ok, err = coroutine.resume(co)
if not ok then
promise:reject(err)
end
return promise
end
---Wrap a function to run asynchronously
---Takes a function and returns a wrapped version that returns a Promise
---@generic T
---@param fn fun(...): T
---@return fun(...): Promise<T>
function Promise.async(fn)
return function(...)
-- Capture both args and count to handle nil values correctly
local n = select('#', ...)
local args = { ... }
return Promise.spawn(function()
return fn(unpack(args, 1, n))
end)
end
end
---Wrap vim.system in a promise
---@generic T
---@param cmd table vim.system cmd options
---@param opts table|nil vim.system opts
---@return Promise<T>
function Promise.system(cmd, opts)
local p = Promise.new()
vim.system(cmd, opts or {}, function(result)
if result.code == 0 then
p:resolve(result)
else
p:reject(result)
end
end)
return p
end
---Wait for all promises to resolve
---Returns a promise that resolves with a table of all results
---If any promise rejects, the returned promise rejects with that error
---@generic T
---@param promises Promise<T>[]
---@return Promise<T[]>
function Promise.all(promises)
return Promise.spawn(function()
local results = {}
for i, promise in ipairs(promises) do
results[i] = promise:await()
end
return results
end)
end
return Promise