@push.rocks/smartbucket
Version:
A TypeScript library providing a cloud-agnostic interface for managing object storage with functionalities like bucket management, file and directory operations, and advanced features such as metadata handling and file locking.
237 lines • 16.4 kB
JavaScript
// classes.watcher.ts
import * as plugins from './plugins.js';
import * as interfaces from './interfaces.js';
import { EventEmitter } from 'node:events';
/**
* BucketWatcher monitors a storage bucket for changes (add/modify/delete)
* using a polling-based approach. Designed to follow the SmartdataDbWatcher pattern.
*
* @example
* ```typescript
* const watcher = bucket.createWatcher({ prefix: 'uploads/', pollIntervalMs: 3000 });
*
* // RxJS Observable pattern
* watcher.changeSubject.subscribe((change) => {
* console.log('Change:', change);
* });
*
* // EventEmitter pattern
* watcher.on('change', (change) => console.log(change));
* watcher.on('error', (err) => console.error(err));
*
* await watcher.start();
* await watcher.readyDeferred.promise; // Wait for initial state
*
* // Later...
* await watcher.stop();
* ```
*/
export class BucketWatcher extends EventEmitter {
constructor(bucketRef, options = {}) {
super();
/** Deferred that resolves when initial state is built and watcher is ready */
this.readyDeferred = plugins.smartpromise.defer();
this.pollIntervalId = null;
this.isPolling = false;
this.isStopped = false;
this.bucketRef = bucketRef;
this.prefix = options.prefix ?? '';
this.pollIntervalMs = options.pollIntervalMs ?? 5000;
this.bufferTimeMs = options.bufferTimeMs;
this.includeInitial = options.includeInitial ?? false;
this.pageSize = options.pageSize ?? 1000;
// Initialize state tracking
this.previousState = new Map();
// Initialize raw subject for emitting changes
this.rawSubject = new plugins.smartrx.rxjs.Subject();
// Configure the public observable with optional buffering
if (this.bufferTimeMs && this.bufferTimeMs > 0) {
this.changeSubject = this.rawSubject.pipe(plugins.smartrx.rxjs.ops.bufferTime(this.bufferTimeMs), plugins.smartrx.rxjs.ops.filter((events) => events.length > 0));
}
else {
this.changeSubject = this.rawSubject.asObservable();
}
}
/**
* Start watching the bucket for changes
*/
async start() {
if (this.pollIntervalId !== null) {
console.log('BucketWatcher is already running');
return;
}
this.isStopped = false;
// Build initial state
await this.buildInitialState();
// Emit initial state as 'add' events if configured
if (this.includeInitial) {
for (const state of this.previousState.values()) {
this.emitChange({
type: 'add',
key: state.key,
size: state.size,
etag: state.etag,
lastModified: state.lastModified,
bucket: this.bucketRef.name,
});
}
}
// Mark as ready
this.readyDeferred.resolve();
// Start polling loop
this.pollIntervalId = setInterval(() => {
this.poll().catch((err) => {
this.emit('error', err);
});
}, this.pollIntervalMs);
}
/**
* Stop watching the bucket
*/
async stop() {
this.isStopped = true;
if (this.pollIntervalId !== null) {
clearInterval(this.pollIntervalId);
this.pollIntervalId = null;
}
// Wait for any in-progress poll to complete
while (this.isPolling) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
this.rawSubject.complete();
}
/**
* Alias for stop() - for consistency with other APIs
*/
async close() {
return this.stop();
}
/**
* Build the initial state by listing all objects with metadata
*/
async buildInitialState() {
this.previousState.clear();
for await (const obj of this.listObjectsWithMetadata()) {
if (obj.Key) {
this.previousState.set(obj.Key, {
key: obj.Key,
etag: obj.ETag ?? '',
size: obj.Size ?? 0,
lastModified: obj.LastModified ?? new Date(0),
});
}
}
}
/**
* Poll for changes by comparing current state against previous state
*/
async poll() {
// Guard against overlapping polls
if (this.isPolling || this.isStopped) {
return;
}
this.isPolling = true;
try {
// Build current state
const currentState = new Map();
for await (const obj of this.listObjectsWithMetadata()) {
if (this.isStopped) {
break;
}
if (obj.Key) {
currentState.set(obj.Key, {
key: obj.Key,
etag: obj.ETag ?? '',
size: obj.Size ?? 0,
lastModified: obj.LastModified ?? new Date(0),
});
}
}
if (!this.isStopped) {
this.detectChanges(currentState);
this.previousState = currentState;
}
}
catch (err) {
this.emit('error', err);
}
finally {
this.isPolling = false;
}
}
/**
* Detect changes between current and previous state
*/
detectChanges(currentState) {
// Detect added and modified objects
for (const [key, current] of currentState) {
const previous = this.previousState.get(key);
if (!previous) {
// New object - emit 'add' event
this.emitChange({
type: 'add',
key: current.key,
size: current.size,
etag: current.etag,
lastModified: current.lastModified,
bucket: this.bucketRef.name,
});
}
else if (previous.etag !== current.etag ||
previous.size !== current.size ||
previous.lastModified.getTime() !== current.lastModified.getTime()) {
// Object modified - emit 'modify' event
this.emitChange({
type: 'modify',
key: current.key,
size: current.size,
etag: current.etag,
lastModified: current.lastModified,
bucket: this.bucketRef.name,
});
}
}
// Detect deleted objects
for (const [key, previous] of this.previousState) {
if (!currentState.has(key)) {
// Object deleted - emit 'delete' event
this.emitChange({
type: 'delete',
key: previous.key,
bucket: this.bucketRef.name,
});
}
}
}
/**
* Emit a change event via both RxJS Subject and EventEmitter
*/
emitChange(event) {
this.rawSubject.next(event);
this.emit('change', event);
}
/**
* List objects with full metadata (ETag, Size, LastModified)
* This is a private method that yields full _Object data, not just keys
*/
async *listObjectsWithMetadata() {
let continuationToken;
do {
if (this.isStopped) {
return;
}
const command = new plugins.s3.ListObjectsV2Command({
Bucket: this.bucketRef.name,
Prefix: this.prefix,
MaxKeys: this.pageSize,
ContinuationToken: continuationToken,
});
const response = await this.bucketRef.smartbucketRef.storageClient.send(command);
for (const obj of response.Contents || []) {
yield obj;
}
continuationToken = response.NextContinuationToken;
} while (continuationToken);
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy53YXRjaGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvY2xhc3Nlcy53YXRjaGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLHFCQUFxQjtBQUVyQixPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUN4QyxPQUFPLEtBQUssVUFBVSxNQUFNLGlCQUFpQixDQUFDO0FBRTlDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFFM0M7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBdUJHO0FBQ0gsTUFBTSxPQUFPLGFBQWMsU0FBUSxZQUFZO0lBc0I3QyxZQUFZLFNBQWlCLEVBQUUsVUFBNEMsRUFBRTtRQUMzRSxLQUFLLEVBQUUsQ0FBQztRQXRCViw4RUFBOEU7UUFDdkUsa0JBQWEsR0FBRyxPQUFPLENBQUMsWUFBWSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBUTVDLG1CQUFjLEdBQTBDLElBQUksQ0FBQztRQUM3RCxjQUFTLEdBQUcsS0FBSyxDQUFDO1FBQ2xCLGNBQVMsR0FBRyxLQUFLLENBQUM7UUFheEIsSUFBSSxDQUFDLFNBQVMsR0FBRyxTQUFTLENBQUM7UUFDM0IsSUFBSSxDQUFDLE1BQU0sR0FBRyxPQUFPLENBQUMsTUFBTSxJQUFJLEVBQUUsQ0FBQztRQUNuQyxJQUFJLENBQUMsY0FBYyxHQUFHLE9BQU8sQ0FBQyxjQUFjLElBQUksSUFBSSxDQUFDO1FBQ3JELElBQUksQ0FBQyxZQUFZLEdBQUcsT0FBTyxDQUFDLFlBQVksQ0FBQztRQUN6QyxJQUFJLENBQUMsY0FBYyxHQUFHLE9BQU8sQ0FBQyxjQUFjLElBQUksS0FBSyxDQUFDO1FBQ3RELElBQUksQ0FBQyxRQUFRLEdBQUcsT0FBTyxDQUFDLFFBQVEsSUFBSSxJQUFJLENBQUM7UUFFekMsNEJBQTRCO1FBQzVCLElBQUksQ0FBQyxhQUFhLEdBQUcsSUFBSSxHQUFHLEVBQUUsQ0FBQztRQUUvQiw4Q0FBOEM7UUFDOUMsSUFBSSxDQUFDLFVBQVUsR0FBRyxJQUFJLE9BQU8sQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBa0MsQ0FBQztRQUVyRiwwREFBMEQ7UUFDMUQsSUFBSSxJQUFJLENBQUMsWUFBWSxJQUFJLElBQUksQ0FBQyxZQUFZLEdBQUcsQ0FBQyxFQUFFLENBQUM7WUFDL0MsSUFBSSxDQUFDLGFBQWEsR0FBRyxJQUFJLENBQUMsVUFBVSxDQUFDLElBQUksQ0FDdkMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsWUFBWSxDQUFDLEVBQ3RELE9BQU8sQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQyxNQUF3QyxFQUFFLEVBQUUsQ0FBQyxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxDQUNqRyxDQUFDO1FBQ0osQ0FBQzthQUFNLENBQUM7WUFDTixJQUFJLENBQUMsYUFBYSxHQUFHLElBQUksQ0FBQyxVQUFVLENBQUMsWUFBWSxFQUFFLENBQUM7UUFDdEQsQ0FBQztJQUNILENBQUM7SUFFRDs7T0FFRztJQUNJLEtBQUssQ0FBQyxLQUFLO1FBQ2hCLElBQUksSUFBSSxDQUFDLGNBQWMsS0FBSyxJQUFJLEVBQUUsQ0FBQztZQUNqQyxPQUFPLENBQUMsR0FBRyxDQUFDLGtDQUFrQyxDQUFDLENBQUM7WUFDaEQsT0FBTztRQUNULENBQUM7UUFFRCxJQUFJLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQztRQUV2QixzQkFBc0I7UUFDdEIsTUFBTSxJQUFJLENBQUMsaUJBQWlCLEVBQUUsQ0FBQztRQUUvQixtREFBbUQ7UUFDbkQsSUFBSSxJQUFJLENBQUMsY0FBYyxFQUFFLENBQUM7WUFDeEIsS0FBSyxNQUFNLEtBQUssSUFBSSxJQUFJLENBQUMsYUFBYSxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUM7Z0JBQ2hELElBQUksQ0FBQyxVQUFVLENBQUM7b0JBQ2QsSUFBSSxFQUFFLEtBQUs7b0JBQ1gsR0FBRyxFQUFFLEtBQUssQ0FBQyxHQUFHO29CQUNkLElBQUksRUFBRSxLQUFLLENBQUMsSUFBSTtvQkFDaEIsSUFBSSxFQUFFLEtBQUssQ0FBQyxJQUFJO29CQUNoQixZQUFZLEVBQUUsS0FBSyxDQUFDLFlBQVk7b0JBQ2hDLE1BQU0sRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLElBQUk7aUJBQzVCLENBQUMsQ0FBQztZQUNMLENBQUM7UUFDSCxDQUFDO1FBRUQsZ0JBQWdCO1FBQ2hCLElBQUksQ0FBQyxhQUFhLENBQUMsT0FBTyxFQUFFLENBQUM7UUFFN0IscUJBQXFCO1FBQ3JCLElBQUksQ0FBQyxjQUFjLEdBQUcsV0FBVyxDQUFDLEdBQUcsRUFBRTtZQUNyQyxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUU7Z0JBQ3hCLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLEdBQUcsQ0FBQyxDQUFDO1lBQzFCLENBQUMsQ0FBQyxDQUFDO1FBQ0wsQ0FBQyxFQUFFLElBQUksQ0FBQyxjQUFjLENBQUMsQ0FBQztJQUMxQixDQUFDO0lBRUQ7O09BRUc7SUFDSSxLQUFLLENBQUMsSUFBSTtRQUNmLElBQUksQ0FBQyxTQUFTLEdBQUcsSUFBSSxDQUFDO1FBRXRCLElBQUksSUFBSSxDQUFDLGNBQWMsS0FBSyxJQUFJLEVBQUUsQ0FBQztZQUNqQyxhQUFhLENBQUMsSUFBSSxDQUFDLGNBQWMsQ0FBQyxDQUFDO1lBQ25DLElBQUksQ0FBQyxjQUFjLEdBQUcsSUFBSSxDQUFDO1FBQzdCLENBQUM7UUFFRCw0Q0FBNEM7UUFDNUMsT0FBTyxJQUFJLENBQUMsU0FBUyxFQUFFLENBQUM7WUFDdEIsTUFBTSxJQUFJLE9BQU8sQ0FBTyxDQUFDLE9BQU8sRUFBRSxFQUFFLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDO1FBQ2hFLENBQUM7UUFFRCxJQUFJLENBQUMsVUFBVSxDQUFDLFFBQVEsRUFBRSxDQUFDO0lBQzdCLENBQUM7SUFFRDs7T0FFRztJQUNJLEtBQUssQ0FBQyxLQUFLO1FBQ2hCLE9BQU8sSUFBSSxDQUFDLElBQUksRUFBRSxDQUFDO0lBQ3JCLENBQUM7SUFFRDs7T0FFRztJQUNLLEtBQUssQ0FBQyxpQkFBaUI7UUFDN0IsSUFBSSxDQUFDLGFBQWEsQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUUzQixJQUFJLEtBQUssRUFBRSxNQUFNLEdBQUcsSUFBSSxJQUFJLENBQUMsdUJBQXVCLEVBQUUsRUFBRSxDQUFDO1lBQ3ZELElBQUksR0FBRyxDQUFDLEdBQUcsRUFBRSxDQUFDO2dCQUNaLElBQUksQ0FBQyxhQUFhLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUU7b0JBQzlCLEdBQUcsRUFBRSxHQUFHLENBQUMsR0FBRztvQkFDWixJQUFJLEVBQUUsR0FBRyxDQUFDLElBQUksSUFBSSxFQUFFO29CQUNwQixJQUFJLEVBQUUsR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDO29CQUNuQixZQUFZLEVBQUUsR0FBRyxDQUFDLFlBQVksSUFBSSxJQUFJLElBQUksQ0FBQyxDQUFDLENBQUM7aUJBQzlDLENBQUMsQ0FBQztZQUNMLENBQUM7UUFDSCxDQUFDO0lBQ0gsQ0FBQztJQUVEOztPQUVHO0lBQ0ssS0FBSyxDQUFDLElBQUk7UUFDaEIsa0NBQWtDO1FBQ2xDLElBQUksSUFBSSxDQUFDLFNBQVMsSUFBSSxJQUFJLENBQUMsU0FBUyxFQUFFLENBQUM7WUFDckMsT0FBTztRQUNULENBQUM7UUFFRCxJQUFJLENBQUMsU0FBUyxHQUFHLElBQUksQ0FBQztRQUV0QixJQUFJLENBQUM7WUFDSCxzQkFBc0I7WUFDdEIsTUFBTSxZQUFZLEdBQUcsSUFBSSxHQUFHLEVBQTBDLENBQUM7WUFFdkUsSUFBSSxLQUFLLEVBQUUsTUFBTSxHQUFHLElBQUksSUFBSSxDQUFDLHVCQUF1QixFQUFFLEVBQUUsQ0FBQztnQkFDdkQsSUFBSSxJQUFJLENBQUMsU0FBUyxFQUFFLENBQUM7b0JBQ25CLE1BQU07Z0JBQ1IsQ0FBQztnQkFFRCxJQUFJLEdBQUcsQ0FBQyxHQUFHLEVBQUUsQ0FBQztvQkFDWixZQUFZLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUU7d0JBQ3hCLEdBQUcsRUFBRSxHQUFHLENBQUMsR0FBRzt3QkFDWixJQUFJLEVBQUUsR0FBRyxDQUFDLElBQUksSUFBSSxFQUFFO3dCQUNwQixJQUFJLEVBQUUsR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDO3dCQUNuQixZQUFZLEVBQUUsR0FBRyxDQUFDLFlBQVksSUFBSSxJQUFJLElBQUksQ0FBQyxDQUFDLENBQUM7cUJBQzlDLENBQUMsQ0FBQztnQkFDTCxDQUFDO1lBQ0gsQ0FBQztZQUVELElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUyxFQUFFLENBQUM7Z0JBQ3BCLElBQUksQ0FBQyxhQUFhLENBQUMsWUFBWSxDQUFDLENBQUM7Z0JBQ2pDLElBQUksQ0FBQyxhQUFhLEdBQUcsWUFBWSxDQUFDO1lBQ3BDLENBQUM7UUFDSCxDQUFDO1FBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztZQUNiLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLEdBQUcsQ0FBQyxDQUFDO1FBQzFCLENBQUM7Z0JBQVMsQ0FBQztZQUNULElBQUksQ0FBQyxTQUFTLEdBQUcsS0FBSyxDQUFDO1FBQ3pCLENBQUM7SUFDSCxDQUFDO0lBRUQ7O09BRUc7SUFDSyxhQUFhLENBQUMsWUFBeUQ7UUFDN0Usb0NBQW9DO1FBQ3BDLEtBQUssTUFBTSxDQUFDLEdBQUcsRUFBRSxPQUFPLENBQUMsSUFBSSxZQUFZLEVBQUUsQ0FBQztZQUMxQyxNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMsYUFBYSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUU3QyxJQUFJLENBQUMsUUFBUSxFQUFFLENBQUM7Z0JBQ2QsZ0NBQWdDO2dCQUNoQyxJQUFJLENBQUMsVUFBVSxDQUFDO29CQUNkLElBQUksRUFBRSxLQUFLO29CQUNYLEdBQUcsRUFBRSxPQUFPLENBQUMsR0FBRztvQkFDaEIsSUFBSSxFQUFFLE9BQU8sQ0FBQyxJQUFJO29CQUNsQixJQUFJLEVBQUUsT0FBTyxDQUFDLElBQUk7b0JBQ2xCLFlBQVksRUFBRSxPQUFPLENBQUMsWUFBWTtvQkFDbEMsTUFBTSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSTtpQkFDNUIsQ0FBQyxDQUFDO1lBQ0wsQ0FBQztpQkFBTSxJQUNMLFFBQVEsQ0FBQyxJQUFJLEtBQUssT0FBTyxDQUFDLElBQUk7Z0JBQzlCLFFBQVEsQ0FBQyxJQUFJLEtBQUssT0FBTyxDQUFDLElBQUk7Z0JBQzlCLFFBQVEsQ0FBQyxZQUFZLENBQUMsT0FBTyxFQUFFLEtBQUssT0FBTyxDQUFDLFlBQVksQ0FBQyxPQUFPLEVBQUUsRUFDbEUsQ0FBQztnQkFDRCx3Q0FBd0M7Z0JBQ3hDLElBQUksQ0FBQyxVQUFVLENBQUM7b0JBQ2QsSUFBSSxFQUFFLFFBQVE7b0JBQ2QsR0FBRyxFQUFFLE9BQU8sQ0FBQyxHQUFHO29CQUNoQixJQUFJLEVBQUUsT0FBTyxDQUFDLElBQUk7b0JBQ2xCLElBQUksRUFBRSxPQUFPLENBQUMsSUFBSTtvQkFDbEIsWUFBWSxFQUFFLE9BQU8sQ0FBQyxZQUFZO29CQUNsQyxNQUFNLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJO2lCQUM1QixDQUFDLENBQUM7WUFDTCxDQUFDO1FBQ0gsQ0FBQztRQUVELHlCQUF5QjtRQUN6QixLQUFLLE1BQU0sQ0FBQyxHQUFHLEVBQUUsUUFBUSxDQUFDLElBQUksSUFBSSxDQUFDLGFBQWEsRUFBRSxDQUFDO1lBQ2pELElBQUksQ0FBQyxZQUFZLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUM7Z0JBQzNCLHVDQUF1QztnQkFDdkMsSUFBSSxDQUFDLFVBQVUsQ0FBQztvQkFDZCxJQUFJLEVBQUUsUUFBUTtvQkFDZCxHQUFHLEVBQUUsUUFBUSxDQUFDLEdBQUc7b0JBQ2pCLE1BQU0sRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLElBQUk7aUJBQzVCLENBQUMsQ0FBQztZQUNMLENBQUM7UUFDSCxDQUFDO0lBQ0gsQ0FBQztJQUVEOztPQUVHO0lBQ0ssVUFBVSxDQUFDLEtBQXFDO1FBQ3RELElBQUksQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQzVCLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBQzdCLENBQUM7SUFFRDs7O09BR0c7SUFDSyxLQUFLLENBQUMsQ0FBQyx1QkFBdUI7UUFDcEMsSUFBSSxpQkFBcUMsQ0FBQztRQUUxQyxHQUFHLENBQUM7WUFDRixJQUFJLElBQUksQ0FBQyxTQUFTLEVBQUUsQ0FBQztnQkFDbkIsT0FBTztZQUNULENBQUM7WUFFRCxNQUFNLE9BQU8sR0FBRyxJQUFJLE9BQU8sQ0FBQyxFQUFFLENBQUMsb0JBQW9CLENBQUM7Z0JBQ2xELE1BQU0sRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLElBQUk7Z0JBQzNCLE1BQU0sRUFBRSxJQUFJLENBQUMsTUFBTTtnQkFDbkIsT0FBTyxFQUFFLElBQUksQ0FBQyxRQUFRO2dCQUN0QixpQkFBaUIsRUFBRSxpQkFBaUI7YUFDckMsQ0FBQyxDQUFDO1lBRUgsTUFBTSxRQUFRLEdBQUcsTUFBTSxJQUFJLENBQUMsU0FBUyxDQUFDLGNBQWMsQ0FBQyxhQUFhLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1lBRWpGLEtBQUssTUFBTSxHQUFHLElBQUksUUFBUSxDQUFDLFFBQVEsSUFBSSxFQUFFLEVBQUUsQ0FBQztnQkFDMUMsTUFBTSxHQUFHLENBQUM7WUFDWixDQUFDO1lBRUQsaUJBQWlCLEdBQUcsUUFBUSxDQUFDLHFCQUFxQixDQUFDO1FBQ3JELENBQUMsUUFBUSxpQkFBaUIsRUFBRTtJQUM5QixDQUFDO0NBQ0YifQ==