forked from rethinkdb/rethinkdb
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cursor.coffee
446 lines (370 loc) · 13.9 KB
/
cursor.coffee
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
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
err = require('./errors')
util = require('./util')
protoResponseType = require('./proto-def').Response.ResponseType
Promise = require('bluebird')
EventEmitter = require('events').EventEmitter
# Import some names to this namespace for convenience
ar = util.ar
varar = util.varar
aropt = util.aropt
mkErr = util.mkErr
# setImmediate is not defined in some browsers (including Chrome)
if not setImmediate?
setImmediate = (cb) ->
setTimeout cb, 0
class IterableResult
stackSize: 100
constructor: (conn, token, opts, root) ->
@_conn = conn
@_token = token
@_opts = opts
@_root = root # current query
@_responses = []
@_responseIndex = 0
@_outstandingRequests = 1 # Because we haven't add the response yet
@_iterations = 0
@_endFlag = false
@_contFlag = false
@_closeAsap = false
@_cont = null
@_cbQueue = []
@next = @_next
@each = @_each
_addResponse: (response) ->
if response.t is @_type or response.t is protoResponseType.SUCCESS_SEQUENCE
# We push a "ok" response only if it's not empty
if response.r.length > 0
@_responses.push response
else
@_responses.push response
@_outstandingRequests -= 1
if response.t isnt @_type
# We got an error or a SUCCESS_SEQUENCE
@_endFlag = true
if @_closeCb?
switch response.t
when protoResponseType.COMPILE_ERROR
@_closeCb mkErr(err.RqlRuntimeError, response, @_root)
when protoResponseType.CLIENT_ERROR
@_closeCb mkErr(err.RqlRuntimeError, response, @_root)
when protoResponseType.RUNTIME_ERROR
@_closeCb mkErr(err.RqlRuntimeError, response, @_root)
else
@_closeCb()
@_contFlag = false
if @_closeAsap is false
@_promptNext()
else
@close @_closeCb
@
_getCallback: ->
@_iterations += 1
cb = @_cbQueue.shift()
if @_iterations % @stackSize is @stackSize - 1
immediateCb = ((err, row) -> setImmediate -> cb(err, row))
return immediateCb
else
return cb
_handleRow: ->
response = @_responses[0]
row = util.recursivelyConvertPseudotype(response.r[@_responseIndex], @_opts)
cb = @_getCallback()
@_responseIndex += 1
# If we're done with this response, discard it
if @_responseIndex is response.r.length
@_responses.shift()
@_responseIndex = 0
cb null, row
bufferEmpty: ->
@_responses.length is 0 or @_responses[0].r.length <= @_responseIndex
_promptNext: ->
# If there are no more waiting callbacks, just wait until the next event
while @_cbQueue[0]?
if @bufferEmpty() is true
# We prefetch things here, set `is 0` to avoid prefectch
if @_endFlag is true
cb = @_getCallback()
cb new err.RqlDriverError "No more rows in the cursor."
else if @_responses.length <= 1
@_promptCont()
return
else
# Try to get a row out of the responses
response = @_responses[0]
if @_responses.length is 1
# We're low on data, prebuffer
@_promptCont()
# Error responses are not discarded, and the error will be sent to all future callbacks
switch response.t
when protoResponseType.SUCCESS_PARTIAL
@_handleRow()
when protoResponseType.SUCCESS_SEQUENCE
if response.r.length is 0
@_responses.shift()
else
@_handleRow()
when protoResponseType.COMPILE_ERROR
@_responses.shift()
cb = @_getCallback()
cb mkErr(err.RqlCompileError, response, @_root)
when protoResponseType.CLIENT_ERROR
@_responses.shift()
cb = @_getCallback()
cb mkErr(err.RqlClientError, response, @_root)
when protoResponseType.RUNTIME_ERROR
@_responses.shift()
cb = @_getCallback()
cb mkErr(err.RqlRuntimeError, response, @_root)
else
@_responses.shift()
cb = @_getCallback()
cb new err.RqlDriverError "Unknown response type for cursor"
_promptCont: ->
# Let's ask the server for more data if we haven't already
if (not @_contFlag) and (not @_endFlag) and @_conn.isOpen()
@_contFlag = true
@_outstandingRequests += 1
@_conn._continueQuery(@_token)
## Implement IterableResult
hasNext: ->
throw new err.RqlDriverError "The `hasNext` command has been removed since 1.13. Use `next` instead."
_next: varar 0, 1, (cb) ->
fn = (cb) =>
@_cbQueue.push cb
@_promptNext()
if typeof cb is "function"
fn(cb)
else if cb is undefined
p = new Promise (resolve, reject) ->
cb = (err, result) ->
if (err)
reject(err)
else
resolve(result)
fn(cb)
return p
else
throw new err.RqlDriverError "First argument to `next` must be a function or undefined."
close: varar 0, 1, (cb) ->
new Promise( (resolve, reject) =>
if @_endFlag is true
resolve()
else if not @_closeCb?
@_closeCb = (err) ->
# Clear all callbacks for outstanding requests
while @_cbQueue.length > 0
@_cbQueue.shift()
# The connection uses _outstandingRequests to see
# if it should remove the token for this
# cursor. This states unambiguously that we don't
# care whatever responses return now.
@_outstandingRequests = 0
if (err)
reject(err)
else
resolve()
@_closeAsap = true
@_outstandingRequests += 1
@_conn._endQuery(@_token)
else
@emit 'error', new err.RqlDriverError "This shouldn't happen"
).nodeify cb
_each: varar(1, 2, (cb, onFinished) ->
unless typeof cb is 'function'
throw new err.RqlDriverError "First argument to each must be a function."
if onFinished? and typeof onFinished isnt 'function'
throw new err.RqlDriverError "Optional second argument to each must be a function."
stopFlag = false
self = @
nextCb = (err, data) =>
if stopFlag isnt true
if err?
if err.message is 'No more rows in the cursor.'
if onFinished?
onFinished()
else
cb(err)
else
stopFlag = cb(null, data) is false
@_next nextCb
else if onFinished?
onFinished()
@_next nextCb
)
toArray: varar 0, 1, (cb) ->
fn = (cb) =>
arr = []
eachCb = (err, row) =>
if err?
cb err
else
arr.push(row)
onFinish = (err, ar) =>
cb null, arr
@each eachCb, onFinish
if cb? and typeof cb isnt 'function'
throw new err.RqlDriverError "First argument to `toArray` must be a function or undefined."
new Promise( (resolve, reject) =>
toArrayCb = (err, result) ->
if err?
reject(err)
else
resolve(result)
fn(toArrayCb)
).nodeify cb
_makeEmitter: ->
@emitter = new EventEmitter
@each = ->
throw new err.RqlDriverError "You cannot use the cursor interface and the EventEmitter interface at the same time."
@next = ->
throw new err.RqlDriverError "You cannot use the cursor interface and the EventEmitter interface at the same time."
addListener: (args...) ->
if not @emitter?
@_makeEmitter()
setImmediate => @_each @_eachCb
@emitter.addListener(args...)
on: (args...) ->
if not @emitter?
@_makeEmitter()
setImmediate => @_each @_eachCb
@emitter.on(args...)
once: ->
if not @emitter?
@_makeEmitter()
setImmediate => @_each @_eachCb
@emitter.once(args...)
removeListener: ->
if not @emitter?
@_makeEmitter()
setImmediate => @_each @_eachCb
@emitter.removeListener(args...)
removeAllListeners: ->
if not @emitter?
@_makeEmitter()
setImmediate => @_each @_eachCb
@emitter.removeAllListeners(args...)
setMaxListeners: ->
if not @emitter?
@_makeEmitter()
setImmediate => @_each @_eachCb
@emitter.setMaxListeners(args...)
listeners: ->
if not @emitter?
@_makeEmitter()
setImmediate => @_each @_eachCb
@emitter.listeners(args...)
emit: ->
if not @emitter?
@_makeEmitter()
setImmediate => @_each @_eachCb
@emitter.emit(args...)
_eachCb: (err, data) =>
if err?
@emitter.emit('error', err)
else
@emitter.emit('data', data)
class Cursor extends IterableResult
constructor: ->
@_type = protoResponseType.SUCCESS_PARTIAL
super
toString: ar () -> "[object Cursor]"
class Feed extends IterableResult
constructor: ->
@_type = protoResponseType.SUCCESS_PARTIAL
super
hasNext: ->
throw new err.RqlDriverError "`hasNext` is not available for feeds."
toArray: ->
throw new err.RqlDriverError "`toArray` is not available for feeds."
toString: ar () -> "[object Feed]"
class UnionedFeed extends IterableResult
constructor: ->
@_type = protoResponseType.SUCCESS_PARTIAL
super
hasNext: ->
throw new err.RqlDriverError "`hasNext` is not available for feeds."
toArray: ->
throw new err.RqlDriverError "`toArray` is not available for feeds."
toString: ar () -> "[object UnionedFeed]"
class AtomFeed extends IterableResult
constructor: ->
@_type = protoResponseType.SUCCESS_PARTIAL
super
hasNext: ->
throw new err.RqlDriverError "`hasNext` is not available for feeds."
toArray: ->
throw new err.RqlDriverError "`toArray` is not available for feeds."
toString: ar () -> "[object AtomFeed]"
class OrderByLimitFeed extends IterableResult
constructor: ->
@_type = protoResponseType.SUCCESS_PARTIAL
super
hasNext: ->
throw new err.RqlDriverError "`hasNext` is not available for feeds."
toArray: ->
throw new err.RqlDriverError "`toArray` is not available for feeds."
toString: ar () -> "[object OrderByLimitFeed]"
# Used to wrap array results so they support the same iterable result
# API as cursors.
class ArrayResult extends IterableResult
# We store @__index as soon as the user starts using the cursor interface
_hasNext: ar () ->
if not @__index?
@__index = 0
@__index < @length
_next: varar 0, 1, (cb) ->
fn = (cb) =>
if @_hasNext() is true
self = @
if self.__index%@stackSize is @stackSize-1
# Reset the stack
setImmediate ->
cb(null, self[self.__index++])
else
cb(null, self[self.__index++])
else
cb new err.RqlDriverError "No more rows in the cursor."
new Promise( (resolve, reject) ->
nextCb = (err, result) ->
if (err)
reject(err)
else
resolve(result)
fn(nextCb)
).nodeify cb
toArray: varar 0, 1, (cb) ->
fn = (cb) =>
# IterableResult.toArray would create a copy
if @__index?
cb(null, @.slice(@__index, @.length))
else
cb(null, @)
new Promise( (resolve, reject) ->
toArrayCb = (err, result) ->
if (err)
reject(err)
else
resolve(result)
fn(toArrayCb)
).nodeify cb
close: ->
return @
makeIterable: (response) ->
response.__proto__ = {}
for name, method of ArrayResult.prototype
if name isnt 'constructor'
if name is '_each'
response.__proto__['each'] = method
response.__proto__['_each'] = method
else if name is '_next'
response.__proto__['next'] = method
response.__proto__['_next'] = method
else
response.__proto__[name] = method
response.__proto__.__proto__ = [].__proto__
response
module.exports.Cursor = Cursor
module.exports.Feed = Feed
module.exports.AtomFeed = AtomFeed
module.exports.OrderByLimitFeed = OrderByLimitFeed
module.exports.makeIterable = ArrayResult::makeIterable