@dollhousemcp/mcp-server
Version:
DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.
257 lines • 27.2 kB
JavaScript
/**
* PaginationService - Element-agnostic pagination service
*
* Provides consistent pagination behavior across all element types following
* the patterns established in CollectionSearch.ts.
*
* Key Features:
* - 1-indexed pages (page 1 = first page)
* - Configurable page size with sensible defaults
* - Complete pagination metadata for UI/API responses
* - Input validation and security logging
* - Stateless and injectable via DI
*
* Security:
* - Validates all inputs to prevent DoS attacks
* - Logs validation failures via SecurityMonitor
* - Enforces maximum page size limits
*
* @see src/types/query/types.ts for interface definitions
* @see src/collection/CollectionSearch.ts for pagination patterns
*/
import { logger } from '../../utils/logger.js';
/**
* Constants for pagination configuration
*/
const PAGINATION_CONSTANTS = {
/** Default number of items per page (Issue #299: reduced from 25 for token efficiency) */
DEFAULT_PAGE_SIZE: 20,
/** Maximum allowed page size to prevent DoS */
MAX_PAGE_SIZE: 100,
/** Minimum valid page number (1-indexed) */
MIN_PAGE: 1,
/** Minimum valid page size */
MIN_PAGE_SIZE: 1,
};
/**
* Implementation of IPaginationService providing element-agnostic pagination
*
* This service is stateless and can be safely shared across multiple consumers.
* It follows the pagination patterns from CollectionSearch.ts:
* - 1-indexed pages
* - Default pageSize of 25
* - Maximum pageSize of 100
* - Accurate hasNextPage/hasPrevPage calculations
*
* @template T - Type of items being paginated (can be any type)
*
* @example
* ```typescript
* const service = new PaginationService();
* const result = service.paginate(items, { page: 2, pageSize: 10 });
* console.log(result.items); // items 11-20
* console.log(result.pagination.hasNextPage); // true if more items exist
* ```
*/
export class PaginationService {
/**
* Paginate an array of items
*
* @param items - Complete array of items to paginate
* @param options - Pagination configuration (page, pageSize)
* @returns Paginated result with items and metadata
* @throws {Error} If pagination options are invalid
*
* @example
* ```typescript
* const service = new PaginationService();
*
* // Basic usage with defaults (page 1, pageSize 25)
* const result1 = service.paginate(items);
*
* // Custom page and page size
* const result2 = service.paginate(items, { page: 3, pageSize: 50 });
*
* // Access results
* console.log(result2.items); // Items 101-150
* console.log(result2.pagination.totalPages); // Total number of pages
* console.log(result2.pagination.hasNextPage); // true if page 4 exists
* ```
*/
paginate(items, options) {
// Apply defaults
const page = options?.page ?? PAGINATION_CONSTANTS.MIN_PAGE;
const pageSize = options?.pageSize ?? PAGINATION_CONSTANTS.DEFAULT_PAGE_SIZE;
// Validate inputs
this.validatePaginationOptions(page, pageSize);
// Calculate pagination boundaries
const totalItems = items.length;
const totalPages = Math.ceil(totalItems / pageSize);
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
// Extract items for current page
// Handle edge cases:
// - Empty arrays: slice returns []
// - Page beyond data: slice returns []
// - pageSize larger than total: slice returns remaining items
const pageItems = items.slice(startIndex, endIndex);
// Calculate navigation flags
const hasNextPage = endIndex < totalItems;
const hasPrevPage = page > 1;
// Build metadata
const metadata = {
page,
pageSize,
totalItems,
totalPages,
hasNextPage,
hasPrevPage,
};
return {
items: pageItems,
pagination: metadata,
};
}
/**
* Calculate pagination metadata without returning items
*
* Useful for API responses where you need metadata but already
* have the items, or when you need to provide pagination info
* before fetching the actual data.
*
* @param totalItems - Total number of items in full result set
* @param options - Pagination configuration (page, pageSize)
* @returns Pagination metadata only
* @throws {Error} If pagination options are invalid
*
* @example
* ```typescript
* const service = new PaginationService();
*
* // Calculate metadata for a known total count
* const metadata = service.calculateMetadata(237, { page: 5, pageSize: 25 });
* console.log(metadata.totalPages); // 10
* console.log(metadata.hasNextPage); // true (pages 6-10 exist)
* console.log(metadata.hasPrevPage); // true (pages 1-4 exist)
* ```
*/
calculateMetadata(totalItems, options) {
logger.debug('PaginationService.calculateMetadata called', {
totalItems,
options,
});
// Validate totalItems
if (totalItems < 0) {
const error = new Error('Total items count must be non-negative');
logger.error('PaginationService.calculateMetadata: Invalid total items count', {
totalItems,
reason: 'Negative total items count',
});
throw error;
}
// Apply defaults
const page = options?.page ?? PAGINATION_CONSTANTS.MIN_PAGE;
const pageSize = options?.pageSize ?? PAGINATION_CONSTANTS.DEFAULT_PAGE_SIZE;
// Validate inputs
this.validatePaginationOptions(page, pageSize);
// Calculate metadata
const totalPages = Math.ceil(totalItems / pageSize);
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const hasNextPage = endIndex < totalItems;
const hasPrevPage = page > 1;
const metadata = {
page,
pageSize,
totalItems,
totalPages,
hasNextPage,
hasPrevPage,
};
logger.debug('PaginationService.calculateMetadata completed', metadata);
return metadata;
}
/**
* Validate pagination options
*
* Ensures that page and pageSize values are within acceptable ranges
* to prevent DoS attacks and invalid pagination states.
*
* Security considerations:
* - Page must be >= 1 (1-indexed)
* - PageSize must be >= 1
* - PageSize must be <= MAX_PAGE_SIZE (prevents memory exhaustion)
*
* @param page - Page number to validate
* @param pageSize - Page size to validate
* @throws {Error} If validation fails
*
* @private
*/
validatePaginationOptions(page, pageSize) {
// Validate page number
if (!Number.isInteger(page) || page < PAGINATION_CONSTANTS.MIN_PAGE) {
const error = new Error(`Page number must be an integer >= ${PAGINATION_CONSTANTS.MIN_PAGE}, got: ${page}`);
logger.error('PaginationService validation failed: Invalid page number', {
page,
minPage: PAGINATION_CONSTANTS.MIN_PAGE,
reason: 'Page number must be a positive integer',
});
throw error;
}
// Validate page size minimum
if (!Number.isInteger(pageSize) || pageSize < PAGINATION_CONSTANTS.MIN_PAGE_SIZE) {
const error = new Error(`Page size must be an integer >= ${PAGINATION_CONSTANTS.MIN_PAGE_SIZE}, got: ${pageSize}`);
logger.error('PaginationService validation failed: Invalid page size (too small)', {
pageSize,
minPageSize: PAGINATION_CONSTANTS.MIN_PAGE_SIZE,
reason: 'Page size must be a positive integer',
});
throw error;
}
// Validate page size maximum (DoS prevention)
if (pageSize > PAGINATION_CONSTANTS.MAX_PAGE_SIZE) {
const error = new Error(`Page size must be <= ${PAGINATION_CONSTANTS.MAX_PAGE_SIZE}, got: ${pageSize}`);
logger.error('PaginationService validation failed: Excessive page size', {
pageSize,
maxPageSize: PAGINATION_CONSTANTS.MAX_PAGE_SIZE,
reason: 'Page size exceeds maximum allowed (DoS prevention)',
});
throw error;
}
}
}
/**
* Factory function to create a new PaginationService instance
*
* Useful for dependency injection scenarios where you want to ensure
* a fresh instance per consumer.
*
* @template T - Type of items being paginated
* @returns New PaginationService instance
*
* @example
* ```typescript
* const paginationService = createPaginationService<MyElementType>();
* const result = paginationService.paginate(items, { page: 1, pageSize: 25 });
* ```
*/
export function createPaginationService() {
return new PaginationService();
}
/**
* Singleton instance for convenience
*
* Use this when you don't need DI or when sharing a single instance
* across the application is acceptable. Since the service is stateless,
* sharing is safe.
*
* @example
* ```typescript
* import { paginationService } from './PaginationService.js';
*
* const result = paginationService.paginate(items, { page: 2, pageSize: 50 });
* ```
*/
export const paginationService = new PaginationService();
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiUGFnaW5hdGlvblNlcnZpY2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvc2VydmljZXMvcXVlcnkvUGFnaW5hdGlvblNlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBb0JHO0FBR0gsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBRS9DOztHQUVHO0FBQ0gsTUFBTSxvQkFBb0IsR0FBRztJQUMzQiwwRkFBMEY7SUFDMUYsaUJBQWlCLEVBQUUsRUFBRTtJQUNyQiwrQ0FBK0M7SUFDL0MsYUFBYSxFQUFFLEdBQUc7SUFDbEIsNENBQTRDO0lBQzVDLFFBQVEsRUFBRSxDQUFDO0lBQ1gsOEJBQThCO0lBQzlCLGFBQWEsRUFBRSxDQUFDO0NBQ1IsQ0FBQztBQUVYOzs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBbUJHO0FBQ0gsTUFBTSxPQUFPLGlCQUFpQjtJQUM1Qjs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7T0F1Qkc7SUFDSSxRQUFRLENBQUMsS0FBVSxFQUFFLE9BQTJCO1FBQ3JELGlCQUFpQjtRQUNqQixNQUFNLElBQUksR0FBRyxPQUFPLEVBQUUsSUFBSSxJQUFJLG9CQUFvQixDQUFDLFFBQVEsQ0FBQztRQUM1RCxNQUFNLFFBQVEsR0FBRyxPQUFPLEVBQUUsUUFBUSxJQUFJLG9CQUFvQixDQUFDLGlCQUFpQixDQUFDO1FBRTdFLGtCQUFrQjtRQUNsQixJQUFJLENBQUMseUJBQXlCLENBQUMsSUFBSSxFQUFFLFFBQVEsQ0FBQyxDQUFDO1FBRS9DLGtDQUFrQztRQUNsQyxNQUFNLFVBQVUsR0FBRyxLQUFLLENBQUMsTUFBTSxDQUFDO1FBQ2hDLE1BQU0sVUFBVSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxHQUFHLFFBQVEsQ0FBQyxDQUFDO1FBQ3BELE1BQU0sVUFBVSxHQUFHLENBQUMsSUFBSSxHQUFHLENBQUMsQ0FBQyxHQUFHLFFBQVEsQ0FB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Z2luYXRpb25TZXJ2aWNlIC0gRWxlbWVudC1hZ25vc3RpYyBwYWdpbmF0aW9uIHNlcnZpY2VcbiAqXG4gKiBQcm92aWRlcyBjb25zaXN0ZW50IHBhZ2luYXRpb24gYmVoYXZpb3IgYWNyb3NzIGFsbCBlbGVtZW50IHR5cGVzIGZvbGxvd2luZ1xuICogdGhlIHBhdHRlcm5zIGVzdGFibGlzaGVkIGluIENvbGxlY3Rpb25TZWFyY2gudHMuXG4gKlxuICogS2V5IEZlYXR1cmVzOlxuICogLSAxLWluZGV4ZWQgcGFnZXMgKHBhZ2UgMSA9IGZpcnN0IHBhZ2UpXG4gKiAtIENvbmZpZ3VyYWJsZSBwYWdlIHNpemUgd2l0aCBzZW5zaWJsZSBkZWZhdWx0c1xuICogLSBDb21wbGV0ZSBwYWdpbmF0aW9uIG1ldGFkYXRhIGZvciBVSS9BUEkgcmVzcG9uc2VzXG4gKiAtIElucHV0IHZhbGlkYXRpb24gYW5kIHNlY3VyaXR5IGxvZ2dpbmdcbiAqIC0gU3RhdGVsZXNzIGFuZCBpbmplY3RhYmxlIHZpYSBESVxuICpcbiAqIFNlY3VyaXR5OlxuICogLSBWYWxpZGF0ZXMgYWxsIGlucHV0cyB0byBwcmV2ZW50IERvUyBhdHRhY2tzXG4gKiAtIExvZ3MgdmFsaWRhdGlvbiBmYWlsdXJlcyB2aWEgU2VjdXJpdHlNb25pdG9yXG4gKiAtIEVuZm9yY2VzIG1heGltdW0gcGFnZSBzaXplIGxpbWl0c1xuICpcbiAqIEBzZWUgc3JjL3R5cGVzL3F1ZXJ5L3R5cGVzLnRzIGZvciBpbnRlcmZhY2UgZGVmaW5pdGlvbnNcbiAqIEBzZWUgc3JjL2NvbGxlY3Rpb24vQ29sbGVjdGlvblNlYXJjaC50cyBmb3IgcGFnaW5hdGlvbiBwYXR0ZXJuc1xuICovXG5cbmltcG9ydCB7IElQYWdpbmF0aW9uU2VydmljZSwgUGFnaW5hdGlvbk9wdGlvbnMsIFBhZ2luYXRlZFJlc3VsdCwgUGFnaW5hdGlvbk1ldGFkYXRhIH0gZnJvbSAnLi90eXBlcy5qcyc7XG5pbXBvcnQgeyBsb2dnZXIgfSBmcm9tICcuLi8uLi91dGlscy9sb2dnZXIuanMnO1xuXG4vKipcbiAqIENvbnN0YW50cyBmb3IgcGFnaW5hdGlvbiBjb25maWd1cmF0aW9uXG4gKi9cbmNvbnN0IFBBR0lOQVRJT05fQ09OU1RBTlRTID0ge1xuICAvKiogRGVmYXVsdCBudW1iZXIgb2YgaXRlbXMgcGVyIHBhZ2UgKElzc3VlICMyOTk6IHJlZHVjZWQgZnJvbSAyNSBmb3IgdG9rZW4gZWZmaWNpZW5jeSkgKi9cbiAgREVGQVVMVF9QQUdFX1NJWkU6IDIwLFxuICAvKiogTWF4aW11bSBhbGxvd2VkIHBhZ2Ugc2l6ZSB0byBwcmV2ZW50IERvUyAqL1xuICBNQVhfUEFHRV9TSVpFOiAxMDAsXG4gIC8qKiBNaW5pbXVtIHZhbGlkIHBhZ2UgbnVtYmVyICgxLWluZGV4ZWQpICovXG4gIE1JTl9QQUdFOiAxLFxuICAvKiogTWluaW11bSB2YWxpZCBwYWdlIHNpemUgKi9cbiAgTUlOX1BBR0VfU0laRTogMSxcbn0gYXMgY29uc3Q7XG5cbi8qKlxuICogSW1wbGVtZW50YXRpb24gb2YgSVBhZ2luYXRpb25TZXJ2aWNlIHByb3ZpZGluZyBlbGVtZW50LWFnbm9zdGljIHBhZ2luYXRpb25cbiAqXG4gKiBUaGlzIHNlcnZpY2UgaXMgc3RhdGVsZXNzIGFuZCBjYW4gYmUgc2FmZWx5IHNoYXJlZCBhY3Jvc3MgbXVsdGlwbGUgY29uc3VtZXJzLlxuICogSXQgZm9sbG93cyB0aGUgcGFnaW5hdGlvbiBwYXR0ZXJucyBmcm9tIENvbGxlY3Rpb25TZWFyY2gudHM6XG4gKiAtIDEtaW5kZXhlZCBwYWdlc1xuICogLSBEZWZhdWx0IHBhZ2VTaXplIG9mIDI1XG4gKiAtIE1heGltdW0gcGFnZVNpemUgb2YgMTAwXG4gKiAtIEFjY3VyYXRlIGhhc05leHRQYWdlL2hhc1ByZXZQYWdlIGNhbGN1bGF0aW9uc1xuICpcbiAqIEB0ZW1wbGF0ZSBUIC0gVHlwZSBvZiBpdGVtcyBiZWluZyBwYWdpbmF0ZWQgKGNhbiBiZSBhbnkgdHlwZSlcbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogY29uc3Qgc2VydmljZSA9IG5ldyBQYWdpbmF0aW9uU2VydmljZSgpO1xuICogY29uc3QgcmVzdWx0ID0gc2VydmljZS5wYWdpbmF0ZShpdGVtcywgeyBwYWdlOiAyLCBwYWdlU2l6ZTogMTAgfSk7XG4gKiBjb25zb2xlLmxvZyhyZXN1bHQuaXRlbXMpOyAvLyBpdGVtcyAxMS0yMFxuICogY29uc29sZS5sb2cocmVzdWx0LnBhZ2luYXRpb24uaGFzTmV4dFBhZ2UpOyAvLyB0cnVlIGlmIG1vcmUgaXRlbXMgZXhpc3RcbiAqIGBgYFxuICovXG5leHBvcnQgY2xhc3MgUGFnaW5hdGlvblNlcnZpY2U8VCA9IGFueT4gaW1wbGVtZW50cyBJUGFnaW5hdGlvblNlcnZpY2U8VD4ge1xuICAvKipcbiAgICogUGFnaW5hdGUgYW4gYXJyYXkgb2YgaXRlbXNcbiAgICpcbiAgICogQHBhcmFtIGl0ZW1zIC0gQ29tcGxldGUgYXJyYXkgb2YgaXRlbXMgdG8gcGFnaW5hdGVcbiAgICogQHBhcmFtIG9wdGlvbnMgLSBQYWdpbmF0aW9uIGNvbmZpZ3VyYXRpb24gKHBhZ2UsIHBhZ2VTaXplKVxuICAgKiBAcmV0dXJucyBQYWdpbmF0ZWQgcmVzdWx0IHdpdGggaXRlbXMgYW5kIG1ldGFkYXRhXG4gICAqIEB0aHJvd3Mge0Vycm9yfSBJZiBwYWdpbmF0aW9uIG9wdGlvbnMgYXJlIGludmFsaWRcbiAgICpcbiAgICogQGV4YW1wbGVcbiAgICogYGBgdHlwZXNjcmlwdFxuICAgKiBjb25zdCBzZXJ2aWNlID0gbmV3IFBhZ2luYXRpb25TZXJ2aWNlKCk7XG4gICAqXG4gICAqIC8vIEJhc2ljIHVzYWdlIHdpdGggZGVmYXVsdHMgKHBhZ2UgMSwgcGFnZVNpemUgMjUpXG4gICAqIGNvbnN0IHJlc3VsdDEgPSBzZXJ2aWNlLnBhZ2luYXRlKGl0ZW1zKTtcbiAgICpcbiAgICogLy8gQ3VzdG9tIHBhZ2UgYW5kIHBhZ2Ugc2l6ZVxuICAgKiBjb25zdCByZXN1bHQyID0gc2VydmljZS5wYWdpbmF0ZShpdGVtcywgeyBwYWdlOiAzLCBwYWdlU2l6ZTogNTAgfSk7XG4gICAqXG4gICAqIC8vIEFjY2VzcyByZXN1bHRzXG4gICAqIGNvbnNvbGUubG9nKHJlc3VsdDIuaXRlbXMpOyAvLyBJdGVtcyAxMDEtMTUwXG4gICAqIGNvbnNvbGUubG9nKHJlc3VsdDIucGFnaW5hdGlvbi50b3RhbFBhZ2VzKTsgLy8gVG90YWwgbnVtYmVyIG9mIHBhZ2VzXG4gICAqIGNvbnNvbGUubG9nKHJlc3VsdDIucGFnaW5hdGlvbi5oYXNOZXh0UGFnZSk7IC8vIHRydWUgaWYgcGFnZSA0IGV4aXN0c1xuICAgKiBgYGBcbiAgICovXG4gIHB1YmxpYyBwYWdpbmF0ZShpdGVtczogVFtdLCBvcHRpb25zPzogUGFnaW5hdGlvbk9wdGlvbnMpOiBQYWdpbmF0ZWRSZXN1bHQ8VD4ge1xuICAgIC8vIEFwcGx5IGRlZmF1bHRzXG4gICAgY29uc3QgcGFnZSA9IG9wdGlvbnM/LnBhZ2UgPz8gUEFHSU5BVElPTl9DT05TVEFOVFMuTUlOX1BBR0U7XG4gICAgY29uc3QgcGFnZVNpemUgPSBvcHRpb25zPy5wYWdlU2l6ZSA/PyBQQUdJTkFUSU9OX0NPTlNUQU5UUy5ERUZBVUxUX1BBR0VfU0laRTtcblxuICAgIC8vIFZhbGlkYXRlIGlucHV0c1xuICAgIHRoaXMudmFsaWRhdGVQYWdpbmF0aW9uT3B0aW9ucyhwYWdlLCBwYWdlU2l6ZSk7XG5cbiAgICAvLyBDYWxjdWxhdGUgcGFnaW5hdGlvbiBib3VuZGFyaWVzXG4gICAgY29uc3QgdG90YWxJdGVtcyA9IGl0ZW1zLmxlbmd0aDtcbiAgICBjb25zdCB0b3RhbFBhZ2VzID0gTWF0aC5jZWlsKHRvdGFsSXRlbXMgLyBwYWdlU2l6ZSk7XG4gICAgY29uc3Qgc3RhcnRJbmRleCA9IChwYWdlIC0gMSkgKiBwYWdlU2l6ZTtcbiAgICBjb25zdCBlbmRJbmRleCA9IHN0YXJ0SW5kZXggKyBwYWdlU2l6ZTtcblxuICAgIC8vIEV4dHJhY3QgaXRlbXMgZm9yIGN1cnJlbnQgcGFnZVxuICAgIC8vIEhhbmRsZSBlZGdlIGNhc2VzOlxuICAgIC8vIC0gRW1wdHkgYXJyYXlzOiBzbGljZSByZXR1cm5zIFtdXG4gICAgLy8gLSBQYWdlIGJleW9uZCBkYXRhOiBzbGljZSByZXR1cm5zIFtdXG4gICAgLy8gLSBwYWdlU2l6ZSBsYXJnZXIgdGhhbiB0b3RhbDogc2xpY2UgcmV0dXJucyByZW1haW5pbmcgaXRlbXNcbiAgICBjb25zdCBwYWdlSXRlbXMgPSBpdGVtcy5zbGljZShzdGFydEluZGV4LCBlbmRJbmRleCk7XG5cbiAgICAvLyBDYWxjdWxhdGUgbmF2aWdhdGlvbiBmbGFnc1xuICAgIGNvbnN0IGhhc05leHRQYWdlID0gZW5kSW5kZXggPCB0b3RhbEl0ZW1zO1xuICAgIGNvbnN0IGhhc1ByZXZQYWdlID0gcGFnZSA+IDE7XG5cbiAgICAvLyBCdWlsZCBtZXRhZGF0YVxuICAgIGNvbnN0IG1ldGFkYXRhOiBQYWdpbmF0aW9uTWV0YWRhdGEgPSB7XG4gICAgICBwYWdlLFxuICAgICAgcGFnZVNpemUsXG4gICAgICB0b3RhbEl0ZW1zLFxuICAgICAgdG90YWxQYWdlcyxcbiAgICAgIGhhc05leHRQYWdlLFxuICAgICAgaGFzUHJldlBhZ2UsXG4gICAgfTtcblxuICAgIHJldHVybiB7XG4gICAgICBpdGVtczogcGFnZUl0ZW1zLFxuICAgICAgcGFnaW5hdGlvbjogbWV0YWRhdGEsXG4gICAgfTtcbiAgfVxuXG4gIC8qKlxuICAgKiBDYWxjdWxhdGUgcGFnaW5hdGlvbiBtZXRhZGF0YSB3aXRob3V0IHJldHVybmluZyBpdGVtc1xuICAgKlxuICAgKiBVc2VmdWwgZm9yIEFQSSByZXNwb25zZXMgd2hlcmUgeW91IG5lZWQgbWV0YWRhdGEgYnV0IGFscmVhZHlcbiAgICogaGF2ZSB0aGUgaXRlbXMsIG9yIHdoZW4geW91IG5lZWQgdG8gcHJvdmlkZSBwYWdpbmF0aW9uIGluZm9cbiAgICogYmVmb3JlIGZldGNoaW5nIHRoZSBhY3R1YWwgZGF0YS5cbiAgICpcbiAgICogQHBhcmFtIHRvdGFsSXRlbXMgLSBUb3RhbCBudW1iZXIgb2YgaXRlbXMgaW4gZnVsbCByZXN1bHQgc2V0XG4gICAqIEBwYXJhbSBvcHRpb25zIC0gUGFnaW5hdGlvbiBjb25maWd1cmF0aW9uIChwYWdlLCBwYWdlU2l6ZSlcbiAgICogQHJldHVybnMgUGFnaW5hdGlvbiBtZXRhZGF0YSBvbmx5XG4gICAqIEB0aHJvd3Mge0Vycm9yfSBJZiBwYWdpbmF0aW9uIG9wdGlvbnMgYXJlIGludmFsaWRcbiAgICpcbiAgICogQGV4YW1wbGVcbiAgICogYGBgdHlwZXNjcmlwdFxuICAgKiBjb25zdCBzZXJ2aWNlID0gbmV3IFBhZ2luYXRpb25TZXJ2aWNlKCk7XG4gICAqXG4gICAqIC8vIENhbGN1bGF0ZSBtZXRhZGF0YSBmb3IgYSBrbm93biB0b3RhbCBjb3VudFxuICAgKiBjb25zdCBtZXRhZGF0YSA9IHNlcnZpY2UuY2FsY3VsYXRlTWV0YWRhdGEoMjM3LCB7IHBhZ2U6IDUsIHBhZ2VTaXplOiAyNSB9KTtcbiAgICogY29uc29sZS5sb2cobWV0YWRhdGEudG90YWxQYWdlcyk7IC8vIDEwXG4gICAqIGNvbnNvbGUubG9nKG1ldGFkYXRhLmhhc05leHRQYWdlKTsgLy8gdHJ1ZSAocGFnZXMgNi0xMCBleGlzdClcbiAgICogY29uc29sZS5sb2cobWV0YWRhdGEuaGFzUHJldlBhZ2UpOyAvLyB0cnVlIChwYWdlcyAxLTQgZXhpc3QpXG4gICAqIGBgYFxuICAgKi9cbiAgcHVibGljIGNhbGN1bGF0ZU1ldGFkYXRhKHRvdGFsSXRlbXM6IG51bWJlciwgb3B0aW9ucz86IFBhZ2luYXRpb25PcHRpb25zKTogUGFnaW5hdGlvbk1ldGFkYXRhIHtcbiAgICBsb2dnZXIuZGVidWcoJ1BhZ2luYXRpb25TZXJ2aWNlLmNhbGN1bGF0ZU1ldGFkYXRhIGNhbGxlZCcsIHtcbiAgICAgIHRvdGFsSXRlbXMsXG4gICAgICBvcHRpb25zLFxuICAgIH0pO1xuXG4gICAgLy8gVmFsaWRhdGUgdG90YWxJdGVtc1xuICAgIGlmICh0b3RhbEl0ZW1zIDwgMCkge1xuICAgICAgY29uc3QgZXJyb3IgPSBuZXcgRXJyb3IoJ1RvdGFsIGl0ZW1zIGNvdW50IG11c3QgYmUgbm9uLW5lZ2F0aXZlJyk7XG4gICAgICBsb2dnZXIuZXJyb3IoJ1BhZ2luYXRpb25TZXJ2aWNlLmNhbGN1bGF0ZU1ldGFkYXRhOiBJbnZhbGlkIHRvdGFsIGl0ZW1zIGNvdW50Jywge1xuICAgICAgICB0b3RhbEl0ZW1zLFxuICAgICAgICByZWFzb246ICdOZWdhdGl2ZSB0b3RhbCBpdGVtcyBjb3VudCcsXG4gICAgICB9KTtcbiAgICAgIHRocm93IGVycm9yO1xuICAgIH1cblxuICAgIC8vIEFwcGx5IGRlZmF1bHRzXG4gICAgY29uc3QgcGFnZSA9IG9wdGlvbnM/LnBhZ2UgPz8gUEFHSU5BVElPTl9DT05TVEFOVFMuTUlOX1BBR0U7XG4gICAgY29uc3QgcGFnZVNpemUgPSBvcHRpb25zPy5wYWdlU2l6ZSA/PyBQQUdJTkFUSU9OX0NPTlNUQU5UUy5ERUZBVUxUX1BBR0VfU0laRTtcblxuICAgIC8vIFZhbGlkYXRlIGlucHV0c1xuICAgIHRoaXMudmFsaWRhdGVQYWdpbmF0aW9uT3B0aW9ucyhwYWdlLCBwYWdlU2l6ZSk7XG5cbiAgICAvLyBDYWxjdWxhdGUgbWV0YWRhdGFcbiAgICBjb25zdCB0b3RhbFBhZ2VzID0gTWF0aC5jZWlsKHRvdGFsSXRlbXMgLyBwYWdlU2l6ZSk7XG4gICAgY29uc3Qgc3RhcnRJbmRleCA9IChwYWdlIC0gMSkgKiBwYWdlU2l6ZTtcbiAgICBjb25zdCBlbmRJbmRleCA9IHN0YXJ0SW5kZXggKyBwYWdlU2l6ZTtcbiAgICBjb25zdCBoYXNOZXh0UGFnZSA9IGVuZEluZGV4IDwgdG90YWxJdGVtcztcbiAgICBjb25zdCBoYXNQcmV2UGFnZSA9IHBhZ2UgPiAxO1xuXG4gICAgY29uc3QgbWV0YWRhdGE6IFBhZ2luYXRpb25NZXRhZGF0YSA9IHtcbiAgICAgIHBhZ2UsXG4gICAgICBwYWdlU2l6ZSxcbiAgICAgIHRvdGFsSXRlbXMsXG4gICAgICB0b3RhbFBhZ2VzLFxuICAgICAgaGFzTmV4dFBhZ2UsXG4gICAgICBoYXNQcmV2UGFnZSxcbiAgICB9O1xuXG4gICAgbG9nZ2VyLmRlYnVnKCdQYWdpbmF0aW9uU2VydmljZS5jYWxjdWxhdGVNZXRhZGF0YSBjb21wbGV0ZWQnLCBtZXRhZGF0YSk7XG5cbiAgICByZXR1cm4gbWV0YWRhdGE7XG4gIH1cblxuICAvKipcbiAgICogVmFsaWRhdGUgcGFnaW5hdGlvbiBvcHRpb25zXG4gICAqXG4gICAqIEVuc3VyZXMgdGhhdCBwYWdlIGFuZCBwYWdlU2l6ZSB2YWx1ZXMgYXJlIHdpdGhpbiBhY2NlcHRhYmxlIHJhbmdlc1xuICAgKiB0byBwcmV2ZW50IERvUyBhdHRhY2tzIGFuZCBpbnZhbGlkIHBhZ2luYXRpb24gc3RhdGVzLlxuICAgKlxuICAgKiBTZWN1cml0eSBjb25zaWRlcmF0aW9uczpcbiAgICogLSBQYWdlIG11c3QgYmUgPj0gMSAoMS1pbmRleGVkKVxuICAgKiAtIFBhZ2VTaXplIG11c3QgYmUgPj0gMVxuICAgKiAtIFBhZ2VTaXplIG11c3QgYmUgPD0gTUFYX1BBR0VfU0laRSAocHJldmVudHMgbWVtb3J5IGV4aGF1c3Rpb24pXG4gICAqXG4gICAqIEBwYXJhbSBwYWdlIC0gUGFnZSBudW1iZXIgdG8gdmFsaWRhdGVcbiAgICogQHBhcmFtIHBhZ2VTaXplIC0gUGFnZSBzaXplIHRvIHZhbGlkYXRlXG4gICAqIEB0aHJvd3Mge0Vycm9yfSBJZiB2YWxpZGF0aW9uIGZhaWxzXG4gICAqXG4gICAqIEBwcml2YXRlXG4gICAqL1xuICBwcml2YXRlIHZhbGlkYXRlUGFnaW5hdGlvbk9wdGlvbnMocGFnZTogbnVtYmVyLCBwYWdlU2l6ZTogbnVtYmVyKTogdm9pZCB7XG4gICAgLy8gVmFsaWRhdGUgcGFnZSBudW1iZXJcbiAgICBpZiAoIU51bWJlci5pc0ludGVnZXIocGFnZSkgfHwgcGFnZSA8IFBBR0lOQVRJT05fQ09OU1RBTlRTLk1JTl9QQUdFKSB7XG4gICAgICBjb25zdCBlcnJvciA9IG5ldyBFcnJvcihcbiAgICAgICAgYFBhZ2UgbnVtYmVyIG11c3QgYmUgYW4gaW50ZWdlciA+PSAke1BBR0lOQVRJT05fQ09OU1RBTlRTLk1JTl9QQUdFfSwgZ290OiAke3BhZ2V9YFxuICAgICAgKTtcbiAgICAgIGxvZ2dlci5lcnJvcignUGFnaW5hdGlvblNlcnZpY2UgdmFsaWRhdGlvbiBmYWlsZWQ6IEludmFsaWQgcGFnZSBudW1iZXInLCB7XG4gICAgICAgIHBhZ2UsXG4gICAgICAgIG1pblBhZ2U6IFBBR0lOQVRJT05fQ09OU1RBTlRTLk1JTl9QQUdFLFxuICAgICAgICByZWFzb246ICdQYWdlIG51bWJlciBtdXN0IGJlIGEgcG9zaXRpdmUgaW50ZWdlcicsXG4gICAgICB9KTtcbiAgICAgIHRocm93IGVycm9yO1xuICAgIH1cblxuICAgIC8vIFZhbGlkYXRlIHBhZ2Ugc2l6ZSBtaW5pbXVtXG4gICAgaWYgKCFOdW1iZXIuaXNJbnRlZ2VyKHBhZ2VTaXplKSB8fCBwYWdlU2l6ZSA8IFBBR0lOQVRJT05fQ09OU1RBTlRTLk1JTl9QQUdFX1NJWkUpIHtcbiAgICAgIGNvbnN0IGVycm9yID0gbmV3IEVycm9yKFxuICAgICAgICBgUGFnZSBzaXplIG11c3QgYmUgYW4gaW50ZWdlciA+PSAke1BBR0lOQVRJT05fQ09OU1RBTlRTLk1JTl9QQUdFX1NJWkV9LCBnb3Q6ICR7cGFnZVNpemV9YFxuICAgICAgKTtcbiAgICAgIGxvZ2dlci5lcnJvcignUGFnaW5hdGlvblNlcnZpY2UgdmFsaWRhdGlvbiBmYWlsZWQ6IEludmFsaWQgcGFnZSBzaXplICh0b28gc21hbGwpJywge1xuICAgICAgICBwYWdlU2l6ZSxcbiAgICAgICAgbWluUGFnZVNpemU6IFBBR0lOQVRJT05fQ09OU1RBTlRTLk1JTl9QQUdFX1NJWkUsXG4gICAgICAgIHJlYXNvbjogJ1BhZ2Ugc2l6ZSBtdXN0IGJlIGEgcG9zaXRpdmUgaW50ZWdlcicsXG4gICAgICB9KTtcbiAgICAgIHRocm93IGVycm9yO1xuICAgIH1cblxuICAgIC8vIFZhbGlkYXRlIHBhZ2Ugc2l6ZSBtYXhpbXVtIChEb1MgcHJldmVudGlvbilcbiAgICBpZiAocGFnZVNpemUgPiBQQUdJTkFUSU9OX0NPTlNUQU5UUy5NQVhfUEFHRV9TSVpFKSB7XG4gICAgICBjb25zdCBlcnJvciA9IG5ldyBFcnJvcihcbiAgICAgICAgYFBhZ2Ugc2l6ZSBtdXN0IGJlIDw9ICR7UEFHSU5BVElPTl9DT05TVEFOVFMuTUFYX1BBR0VfU0laRX0sIGdvdDogJHtwYWdlU2l6ZX1gXG4gICAgICApO1xuICAgICAgbG9nZ2VyLmVycm9yKCdQYWdpbmF0aW9uU2VydmljZSB2YWxpZGF0aW9uIGZhaWxlZDogRXhjZXNzaXZlIHBhZ2Ugc2l6ZScsIHtcbiAgICAgICAgcGFnZVNpemUsXG4gICAgICAgIG1heFBhZ2VTaXplOiBQQUdJTkFUSU9OX0NPTlNUQU5UUy5NQVhfUEFHRV9TSVpFLFxuICAgICAgICByZWFzb246ICdQYWdlIHNpemUgZXhjZWVkcyBtYXhpbXVtIGFsbG93ZWQgKERvUyBwcmV2ZW50aW9uKScsXG4gICAgICB9KTtcbiAgICAgIHRocm93IGVycm9yO1xuICAgIH1cbiAgfVxufVxuXG4vKipcbiAqIEZhY3RvcnkgZnVuY3Rpb24gdG8gY3JlYXRlIGEgbmV3IFBhZ2luYXRpb25TZXJ2aWNlIGluc3RhbmNlXG4gKlxuICogVXNlZnVsIGZvciBkZXBlbmRlbmN5IGluamVjdGlvbiBzY2VuYXJpb3Mgd2hlcmUgeW91IHdhbnQgdG8gZW5zdXJlXG4gKiBhIGZyZXNoIGluc3RhbmNlIHBlciBjb25zdW1lci5cbiAqXG4gKiBAdGVtcGxhdGUgVCAtIFR5cGUgb2YgaXRlbXMgYmVpbmcgcGFnaW5hdGVkXG4gKiBAcmV0dXJucyBOZXcgUGFnaW5hdGlvblNlcnZpY2UgaW5zdGFuY2VcbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogY29uc3QgcGFnaW5hdGlvblNlcnZpY2UgPSBjcmVhdGVQYWdpbmF0aW9uU2VydmljZTxNeUVsZW1lbnRUeXBlPigpO1xuICogY29uc3QgcmVzdWx0ID0gcGFnaW5hdGlvblNlcnZpY2UucGFnaW5hdGUoaXRlbXMsIHsgcGFnZTogMSwgcGFnZVNpemU6IDI1IH0pO1xuICogYGBgXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVQYWdpbmF0aW9uU2VydmljZTxUID0gYW55PigpOiBJUGFnaW5hdGlvblNlcnZpY2U8VD4ge1xuICByZXR1cm4gbmV3IFBhZ2luYXRpb25TZXJ2aWNlPFQ+KCk7XG59XG5cbi8qKlxuICogU2luZ2xldG9uIGluc3RhbmNlIGZvciBjb252ZW5pZW5jZVxuICpcbiAqIFVzZSB0aGlzIHdoZW4geW91IGRvbid0IG5lZWQgREkgb3Igd2hlbiBzaGFyaW5nIGEgc2luZ2xlIGluc3RhbmNlXG4gKiBhY3Jvc3MgdGhlIGFwcGxpY2F0aW9uIGlzIGFjY2VwdGFibGUuIFNpbmNlIHRoZSBzZXJ2aWNlIGlzIHN0YXRlbGVzcyxcbiAqIHNoYXJpbmcgaXMgc2FmZS5cbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogaW1wb3J0IHsgcGFnaW5hdGlvblNlcnZpY2UgfSBmcm9tICcuL1BhZ2luYXRpb25TZXJ2aWNlLmpzJztcbiAqXG4gKiBjb25zdCByZXN1bHQgPSBwYWdpbmF0aW9uU2VydmljZS5wYWdpbmF0ZShpdGVtcywgeyBwYWdlOiAyLCBwYWdlU2l6ZTogNTAgfSk7XG4gKiBgYGBcbiAqL1xuZXhwb3J0IGNvbnN0IHBhZ2luYXRpb25TZXJ2aWNlID0gbmV3IFBhZ2luYXRpb25TZXJ2aWNlKCk7XG4iXX0=