forked from zserge/lua-promises
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeferred.lua
More file actions
325 lines (304 loc) · 8.19 KB
/
deferred.lua
File metadata and controls
325 lines (304 loc) · 8.19 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
--- A+ promises in Lua.
--- @module deferred
local class = _G['class'] or require('middleclass')
local Deferred = class('Deferred')
local PENDING = 0
local RESOLVING = 1
local REJECTING = 2
local RESOLVED = 3
local REJECTED = 4
local _is_function = function(f)
if type(f) == 'table' then
local mt = getmetatable(f)
return mt ~= nil and type(mt.__call) == 'function'
end
return type(f) == 'function'
end
--- Returns a new promise object.
--- @treturn Promise New promise
--- @usage
--- local deferred = require('deferred')
---
--- --
--- -- Converting callback-based API into promise-based is very straightforward:
--- --
--- -- 1) Create promise object
--- -- 2) Start your asynchronous action
--- -- 3) Resolve promise object whenever action is finished (only first resolution
--- -- is accepted, others are ignored)
--- -- 4) Reject promise object whenever action is failed (only first rejection is
--- -- accepted, others are ignored)
--- -- 5) Return promise object letting calling side to add a chain of callbacks to
--- -- your asynchronous function
---
--- function read(f)
--- local d = deferred.new()
--- readasync(f, function(contents, err)
--- if err == nil then
--- d:resolve(contents)
--- else
--- d:reject(err)
--- end
--- end)
--- return d
--- end
---
--- -- You can now use read() like this:
--- read('file.txt'):next(function(s)
--- print('File.txt contents: ', s)
--- end, function(err)
--- print('Error', err)
--- end)
function Deferred:initialize(options)
if _is_function(options) then
self:initialize()
local ok, err = pcall(options, self)
if not ok then
self:reject(err)
end
else
options = options or {}
self.next = function(self, success, failure)
local next = Deferred:new({success = success, failure = failure, extend = options.extend})
if self.state == RESOLVED then
next:resolve(self.value)
elseif self.state == REJECTED then
next:reject(self.value)
else
table.insert(self.queue, next)
end
return next
end
self.state = 0
self.queue = {}
self.success = options.success
self.failure = options.failure
if _is_function(options.extend) then
options.extend(self)
end
end
end
local _finish = function(deferred, state)
state = state or REJECTED
for i, f in ipairs(deferred.queue) do
if state == RESOLVED then
f:resolve(deferred.value)
else
f:reject(deferred.value)
end
end
deferred.state = state
end
local _promise = function(deferred, next, success, failure, nonpromisecb)
if type(deferred) == 'table' and type(deferred.value) == 'table' and _is_function(next) then
local called = false
local ok, err = pcall(next, deferred.value, function(v)
if called then return end
called = true
deferred.value = v
success()
end, function(v)
if called then return end
called = true
deferred.value = v
failure()
end)
if not ok and not called then
deferred.value = err
failure()
end
else
nonpromisecb()
end
end
local _fire = function(deferred)
local next
if type(deferred.value) == 'table' then
next = deferred.value.next
end
_promise(deferred, next, function()
deferred.state = RESOLVING
_fire(deferred)
end, function()
deferred.state = REJECTING
_fire(deferred)
end, function()
local ok
local v
if deferred.state == RESOLVING and _is_function(deferred.success) then
ok, v = pcall(deferred.success, deferred.value)
elseif deferred.state == REJECTING and _is_function(deferred.failure) then
ok, v = pcall(deferred.failure, deferred.value)
if ok then
deferred.state = RESOLVING
end
end
if ok ~= nil then
if ok then
deferred.value = v
else
deferred.value = v
return _finish(deferred)
end
end
if deferred.value == deferred then
deferred.value = pcall(error, 'resolving promise with itself')
return _finish(deferred)
else
_promise(deferred, next, function()
_finish(deferred, RESOLVED)
end, function(state)
_finish(deferred, state)
end, function()
_finish(deferred, deferred.state == RESOLVING and RESOLVED)
end)
end
end)
end
local _resolve = function(deferred, state, value)
if deferred.state == 0 then
deferred.value = value
deferred.state = state
_fire(deferred)
end
return deferred
end
--
-- PUBLIC API
--
function Deferred:resolve(value)
return _resolve(self, RESOLVING, value)
end
function Deferred:reject(value)
return _resolve(self, REJECTING, value)
end
--- Returns a new promise object that is resolved when all promises are resolved/rejected.
--- @param args list of promise
--- @treturn Promise New promise
--- @usage
--- deferred.all({
--- http.get('http://example.com/first'),
--- http.get('http://example.com/second'),
--- http.get('http://example.com/third'),
--- }):next(function(results)
--- -- handle results here (all requests are finished and there has been
--- -- no errors)
--- end, function(results)
--- -- handle errors here (all requests are finished and there has been
--- -- at least one error)
--- end)
function Deferred.static:all(args)
local d = Deferred:new()
if #args == 0 then
return d:resolve({})
end
local method = "resolve"
local pending = #args
local results = {}
local function synchronizer(i, resolved)
return function(value)
results[i] = value
if not resolved then
method = "reject"
end
pending = pending - 1
if pending == 0 then
d[method](d, results)
end
return value
end
end
for i = 1, pending do
args[i]:next(synchronizer(i, true), synchronizer(i, false))
end
return d
end
--- Returns a new promise object that is resolved with the values of sequential application of function fn to each element in the list. fn is expected to return promise object.
--- @function map
--- @param args list of promise
--- @param fn promise used to resolve the list of promise
--- @return a new promise
--- @usage
--- local items = {'a.txt', 'b.txt', 'c.txt'}
--- -- Read 3 files, one by one
--- deferred.map(items, read):next(function(files)
--- -- here files is an array of file contents for each of the files
--- end, function(err)
--- -- handle reading error
--- end)
function Deferred.static:map(args, fn)
local d = Deferred:new()
local results = {}
local function donext(i)
if i > #args then
d:resolve(results)
else
fn(args[i]):next(function(res)
table.insert(results, res)
donext(i+1)
end, function(err)
d:reject(err)
end)
end
end
donext(1)
return d
end
--- Returns a new promise object that is resolved as soon as the first of the promises gets resolved/rejected.
--- @param args list of promise
--- @treturn Promise New promise
--- @usage
--- -- returns a promise that gets rejected after a certain timeout
--- function timeout(sec)
--- local d = deferred.new()
--- settimeout(function()
--- d:reject('Timeout')
--- end, sec)
--- return d
--- end
---
--- deferred.first({
--- read(somefile), -- resolves promise with contents, or rejects with error
--- timeout(5),
--- }):next(function(result)
--- -- file was read successfully...
--- end, function(err)
--- -- either timeout or I/O error...
--- end)
function Deferred.static:first(args)
local d = Deferred:new()
for _, v in ipairs(args) do
v:next(function(res)
d:resolve(res)
end, function(err)
d:reject(err)
end)
end
return d
end
--- A promise is an object that can store a value to be retrieved by a future object.
--- @type Promise
--- Wait for the promise object.
--- @function next
--- @tparam function cb resolve callback (function(value) end)
--- @tparam[opt] function errcb rejection callback (function(reject_value) end)
--- @usage
--- -- Reading two files sequentially:
--- read('first.txt'):next(function(s)
--- print('File file:', s)
--- return read('second.txt')
--- end):next(function(s)
--- print('Second file:', s)
--- end):next(nil, function(err)
--- -- error while reading first or second file
--- print('Error', err)
--- end)
--- Resolve promise object with value.
--- @function resolve
--- @param value promise value
--- @return resolved future result
--- Reject promise object with value.
--- @function reject
--- @param value promise value
--- @return rejected future result
return Deferred