search_pager.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. 'use strict'
  2. const EventEmitter = require('events').EventEmitter
  3. const util = require('util')
  4. const assert = require('assert-plus')
  5. const { PagedResultsControl } = require('@ldapjs/controls')
  6. const CorkedEmitter = require('../corked_emitter.js')
  7. /// --- API
  8. /**
  9. * Handler object for paged search operations.
  10. *
  11. * Provided to consumers in place of the normal search EventEmitter it adds the
  12. * following new events:
  13. * 1. page - Emitted whenever the end of a result page is encountered.
  14. * If this is the last page, 'end' will also be emitted.
  15. * The event passes two arguments:
  16. * 1. The result object (similar to 'end')
  17. * 2. A callback function optionally used to continue the search
  18. * operation if the pagePause option was specified during
  19. * initialization.
  20. * 2. pageError - Emitted if the server does not support paged search results
  21. * If there are no listeners for this event, the 'error' event
  22. * will be emitted (and 'end' will not be). By listening to
  23. * 'pageError', a successful search that lacks paging will be
  24. * able to emit 'end'.
  25. */
  26. function SearchPager (opts) {
  27. assert.object(opts)
  28. assert.func(opts.callback)
  29. assert.number(opts.pageSize)
  30. assert.func(opts.sendRequest)
  31. CorkedEmitter.call(this, {})
  32. this.callback = opts.callback
  33. this.controls = opts.controls
  34. this.pageSize = opts.pageSize
  35. this.pagePause = opts.pagePause
  36. this.sendRequest = opts.sendRequest
  37. this.controls.forEach(function (control) {
  38. if (control.type === PagedResultsControl.OID) {
  39. // The point of using SearchPager is not having to do this.
  40. // Toss an error if the pagedResultsControl is present
  41. throw new Error('redundant pagedResultControl')
  42. }
  43. })
  44. this.finished = false
  45. this.started = false
  46. const emitter = new EventEmitter()
  47. emitter.on('searchRequest', this.emit.bind(this, 'searchRequest'))
  48. emitter.on('searchEntry', this.emit.bind(this, 'searchEntry'))
  49. emitter.on('end', this._onEnd.bind(this))
  50. emitter.on('error', this._onError.bind(this))
  51. this.childEmitter = emitter
  52. }
  53. util.inherits(SearchPager, CorkedEmitter)
  54. module.exports = SearchPager
  55. /**
  56. * Start the paged search.
  57. */
  58. SearchPager.prototype.begin = function begin () {
  59. // Starting first page
  60. this._nextPage(null)
  61. }
  62. SearchPager.prototype._onEnd = function _onEnd (res) {
  63. const self = this
  64. let cookie = null
  65. res.controls.forEach(function (control) {
  66. if (control.type === PagedResultsControl.OID) {
  67. cookie = control.value.cookie
  68. }
  69. })
  70. // Pass a noop callback by default for page events
  71. const nullCb = function () { }
  72. if (cookie === null) {
  73. // paged search not supported
  74. this.finished = true
  75. this.emit('page', res, nullCb)
  76. const err = new Error('missing paged control')
  77. err.name = 'PagedError'
  78. if (this.listeners('pageError').length > 0) {
  79. this.emit('pageError', err)
  80. // If the consumer as subscribed to pageError, SearchPager is absolved
  81. // from delivering the fault via the 'error' event. Emitting an 'end'
  82. // event after 'error' breaks the contract that the standard client
  83. // provides, so it's only a possibility if 'pageError' is used instead.
  84. this.emit('end', res)
  85. } else {
  86. this.emit('error', err)
  87. // No end event possible per explanation above.
  88. }
  89. return
  90. }
  91. if (cookie.length === 0) {
  92. // end of paged results
  93. this.finished = true
  94. this.emit('page', nullCb)
  95. this.emit('end', res)
  96. } else {
  97. if (this.pagePause) {
  98. // Wait to fetch next page until callback is invoked
  99. // Halt page fetching if called with error
  100. this.emit('page', res, function (err) {
  101. if (!err) {
  102. self._nextPage(cookie)
  103. } else {
  104. // the paged search has been canceled so emit an end
  105. self.emit('end', res)
  106. }
  107. })
  108. } else {
  109. this.emit('page', res, nullCb)
  110. this._nextPage(cookie)
  111. }
  112. }
  113. }
  114. SearchPager.prototype._onError = function _onError (err) {
  115. this.finished = true
  116. this.emit('error', err)
  117. }
  118. /**
  119. * Initiate a search for the next page using the returned cookie value.
  120. */
  121. SearchPager.prototype._nextPage = function _nextPage (cookie) {
  122. const controls = this.controls.slice(0)
  123. controls.push(new PagedResultsControl({
  124. value: {
  125. size: this.pageSize,
  126. cookie
  127. }
  128. }))
  129. this.sendRequest(controls, this.childEmitter, this._sendCallback.bind(this))
  130. }
  131. /**
  132. * Callback provided to the client API for successful transmission.
  133. */
  134. SearchPager.prototype._sendCallback = function _sendCallback (err) {
  135. if (err) {
  136. this.finished = true
  137. if (!this.started) {
  138. // EmitSend error during the first page, bail via callback
  139. this.callback(err, null)
  140. } else {
  141. this.emit('error', err)
  142. }
  143. } else {
  144. // search successfully send
  145. if (!this.started) {
  146. this.started = true
  147. // send self as emitter as the client would
  148. this.callback(null, this)
  149. }
  150. }
  151. }