node-ldapjs/lib/client/search_pager.js

173 lines
5.1 KiB
JavaScript

// Copyright 2014 Joyent, Inc. All rights reserved.
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var assert = require('assert-plus');
var dn = require('../dn');
var messages = require('../messages/index');
var Protocol = require('../protocol');
var PagedControl = require('../controls/paged_results_control.js');
///--- API
/**
* Handler object for paged search operations.
*
* Provided to consumers in place of the normal search EventEmitter it adds the
* following new events:
* 1. page - Emitted whenever the end of a result page is encountered.
* If this is the last page, 'end' will also be emitted.
* The event passes two arguments:
* 1. The result object (similar to 'end')
* 2. A callback function optionally used to continue the search
* operation if the pagePause option was specified during
* initialization.
* 2. pageError - Emitted if the server does not support paged search results
* If there are no listeners for this event, the 'error' event
* will be emitted (and 'end' will not be). By listening to
* 'pageError', a successful search that lacks paging will be
* able to emit 'end'.
* 3. search - Emitted as an internal event to trigger another client search.
*/
function SearchPager(opts) {
assert.object(opts);
assert.func(opts.callback);
assert.number(opts.pageSize);
EventEmitter.call(this, {});
this.callback = opts.callback;
this.controls = opts.controls;
this.pageSize = opts.pageSize;
this.pagePause = opts.pagePause;
this.controls.forEach(function (control) {
if (control.type === PagedControl.OID) {
// The point of using SearchPager is not having to do this.
// Toss an error if the pagedResultsControl is present
throw new Error('redundant pagedResultControl');
}
});
this.finished = false;
this.started = false;
var emitter = new EventEmitter();
emitter.on('searchEntry', this.emit.bind(this, 'searchEntry'));
emitter.on('end', this._onEnd.bind(this));
emitter.on('error', this._onError.bind(this));
this.childEmitter = emitter;
}
util.inherits(SearchPager, EventEmitter);
module.exports = SearchPager;
/**
* Start the paged search.
*/
SearchPager.prototype.begin = function begin() {
// Starting first page
this._nextPage(null);
};
SearchPager.prototype._onEnd = function _onEnd(res) {
var self = this;
var cookie = null;
res.controls.forEach(function (control) {
if (control.type === PagedControl.OID) {
cookie = control.value.cookie;
}
});
// Pass a noop callback by default for page events
var nullCb = function () { };
if (cookie === null) {
// paged search not supported
this.finished = true;
this.emit('page', res, nullCb);
var err = new Error('missing paged control');
err.name = 'PagedError';
if (this.listeners('pageError').length > 0) {
this.emit('pageError', err);
// If the consumer as subscribed to pageError, SearchPager is absolved
// from deliverying the fault via the 'error' event. Emitting an 'end'
// event after 'error' breaks the contract that the standard client
// provides, so it's only a possibility if 'pageError' is used instead.
this.emit('end', res);
} else {
this.emit('error', err);
// No end event possible per explaination above.
}
return;
}
if (cookie.length === 0) {
// end of paged results
this.finished = true;
this.emit('page', nullCb);
this.emit('end', res);
} else {
if (this.pagePause) {
// Wait to fetch next page until callback is invoked
// Halt page fetching if called with error
this.emit('page', res, function (err) {
if (!err) {
self._nextPage(cookie);
} else {
// the paged search has been canceled so emit an end
self.emit('end', res);
}
});
} else {
this.emit('page', res, nullCb);
this._nextPage(cookie);
}
}
};
SearchPager.prototype._onError = function _onError(err) {
this.finished = true;
this.emit('error', err);
};
/**
* Initiate a search for the next page using the returned cookie value.
*/
SearchPager.prototype._nextPage = function _nextPage(cookie) {
var controls = this.controls.slice(0);
controls.push(new PagedControl({
value: {
size: this.pageSize,
cookie: cookie
}
}));
this.emit('search', controls, this.childEmitter,
this._sendCallback.bind(this));
};
/**
* Callback provided to the client API for successful transmission.
*/
SearchPager.prototype._sendCallback = function _sendCallback(err, res) {
if (err) {
this.finished = true;
if (!this.started) {
// EmitSend error during the first page, bail via callback
this.callback(err, null);
} else {
this.emit('error', err);
}
} else {
// search successfully send
if (!this.started) {
this.started = true;
// send self as emitter as the client would
this.callback(null, this);
}
}
};