UNPKG

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