-
Notifications
You must be signed in to change notification settings - Fork 35
/
deferred.lua
333 lines (310 loc) · 8.01 KB
/
deferred.lua
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
--- A+ promises in Lua.
--- @module deferred
local M = {}
local deferred = {}
deferred.__index = deferred
local PENDING = 0
local RESOLVING = 1
local REJECTING = 2
local RESOLVED = 3
local REJECTED = 4
local function finish(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 function isfunction(f)
if type(f) == 'table' then
local mt = getmetatable(f)
return mt ~= nil and type(mt.__call) == 'function'
end
return type(f) == 'function'
end
local function promise(deferred, next, success, failure, nonpromisecb)
if type(deferred) == 'table' and type(deferred.value) == 'table' and isfunction(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 function fire(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 isfunction(deferred.success) then
ok, v = pcall(deferred.success, deferred.value)
elseif deferred.state == REJECTING and isfunction(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 function resolve(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.
--- @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 M.new(options)
if isfunction(options) then
local d = M.new()
local ok, err = pcall(options, d)
if not ok then
d:reject(err)
end
return d
end
options = options or {}
local d
d = {
next = function(self, success, failure)
local next = M.new({success = success, failure = failure, extend = options.extend})
if d.state == RESOLVED then
next:resolve(d.value)
elseif d.state == REJECTED then
next:reject(d.value)
else
table.insert(d.queue, next)
end
return next
end,
state = 0,
queue = {},
success = options.success,
failure = options.failure,
}
d = setmetatable(d, deferred)
if isfunction(options.extend) then
options.extend(d)
end
return d
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 M.all(args)
local d = M.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 M.map(args, fn)
local d = M.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 M.first(args)
local d = M.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 M