UNPKG

@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
// 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy53YXRjaGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvY2xhc3Nlcy53YXRjaGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLHFCQUFxQjtBQUVyQixPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUN4QyxPQUFPLEtBQUssVUFBVSxNQUFNLGlCQUFpQixDQUFDO0FBRTlDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFFM0M7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBdUJHO0FBQ0gsTUFBTSxPQUFPLGFBQWMsU0FBUSxZQUFZO0lBc0I3QyxZQUFZLFNBQWlCLEVBQUUsVUFBNEMsRUFBRTtRQUMzRSxLQUFLLEVBQUUsQ0FBQztRQXRCViw4RUFBOEU7UUFDdkUsa0JBQWEsR0FBRyxPQUFPLENBQUMsWUFBWSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBUTVDLG1CQUFjLEdBQTBDLElBQUksQ0FBQztRQUM3RCxjQUFTLEdBQUcsS0FBSyxDQUFDO1FBQ2xCLGNBQVMsR0FBRyxLQUFLLENBQUM7UUFheEIsSUFBSSxDQUFDLFNBQVMsR0FBRyxTQUFTLENBQUM7UUFDM0IsSUFBSSxDQUFDLE1BQU0sR0FBRyxPQUFPLENBQUMsTUFBTSxJQUFJLEVBQUUsQ0FBQztRQUNuQyxJQUFJLENBQUMsY0FBYyxHQUFHLE9BQU8sQ0FBQyxjQUFjLElBQUksSUFBSSxDQUFDO1FBQ3JELElBQUksQ0FBQyxZQUFZLEdBQUcsT0FBTyxDQUFDLFlBQVksQ0FBQztRQUN6QyxJQUFJLENBQUMsY0FBYyxHQUFHLE9BQU8sQ0FBQyxjQUFjLElBQUksS0FBSyxDQUFDO1FBQ3RELElBQUksQ0FBQyxRQUFRLEdBQUcsT0FBTyxDQUFDLFFBQVEsSUFBSSxJQUFJLENBQUM7UUFFekMsNEJBQTRCO1FBQzVCLElBQUksQ0FBQyxhQUFhLEdBQUcsSUFBSSxHQUFHLEVBQUUsQ0FBQztRQUUvQiw4Q0FBOEM7UUFDOUMsSUFBSSxD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