@lossless.org/client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +37 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.losslessorgclient.d.ts +32 -0
- package/dist_ts/classes.losslessorgclient.js +103 -0
- package/dist_ts/core/classes.error.d.ts +16 -0
- package/dist_ts/core/classes.error.js +29 -0
- package/dist_ts/core/classes.operation.d.ts +17 -0
- package/dist_ts/core/classes.operation.js +70 -0
- package/dist_ts/core/interfaces.d.ts +18 -0
- package/dist_ts/core/interfaces.js +2 -0
- package/dist_ts/index.d.ts +4 -0
- package/dist_ts/index.js +3 -0
- package/dist_ts/nosqldb/classes.atomicdelete.d.ts +26 -0
- package/dist_ts/nosqldb/classes.atomicdelete.js +44 -0
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +27 -0
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +86 -0
- package/dist_ts/nosqldb/classes.atomicupdate.d.ts +26 -0
- package/dist_ts/nosqldb/classes.atomicupdate.js +55 -0
- package/dist_ts/nosqldb/classes.bootstraprecord.d.ts +14 -0
- package/dist_ts/nosqldb/classes.bootstraprecord.js +80 -0
- package/dist_ts/nosqldb/classes.bsonassertion.d.ts +2 -0
- package/dist_ts/nosqldb/classes.bsonassertion.js +29 -0
- package/dist_ts/nosqldb/classes.bsonobjectid.d.ts +2 -0
- package/dist_ts/nosqldb/classes.bsonobjectid.js +50 -0
- package/dist_ts/nosqldb/classes.bsonsnapshot.d.ts +3 -0
- package/dist_ts/nosqldb/classes.bsonsnapshot.js +126 -0
- package/dist_ts/nosqldb/classes.cached-document.d.ts +27 -0
- package/dist_ts/nosqldb/classes.cached-document.js +112 -0
- package/dist_ts/nosqldb/classes.collection.d.ts +302 -0
- package/dist_ts/nosqldb/classes.collection.js +1568 -0
- package/dist_ts/nosqldb/classes.collectionfactory.d.ts +30 -0
- package/dist_ts/nosqldb/classes.collectionfactory.js +65 -0
- package/dist_ts/nosqldb/classes.collectionlifecycle.d.ts +4 -0
- package/dist_ts/nosqldb/classes.collectionlifecycle.js +15 -0
- package/dist_ts/nosqldb/classes.collectionpreparation.d.ts +34 -0
- package/dist_ts/nosqldb/classes.collectionpreparation.js +134 -0
- package/dist_ts/nosqldb/classes.collectiontopology.d.ts +47 -0
- package/dist_ts/nosqldb/classes.collectiontopology.js +388 -0
- package/dist_ts/nosqldb/classes.connection.d.ts +18 -0
- package/dist_ts/nosqldb/classes.connection.js +23 -0
- package/dist_ts/nosqldb/classes.convenience.d.ts +1 -0
- package/dist_ts/nosqldb/classes.convenience.js +5 -0
- package/dist_ts/nosqldb/classes.cursor.d.ts +15 -0
- package/dist_ts/nosqldb/classes.cursor.js +82 -0
- package/dist_ts/nosqldb/classes.cursoroperation.d.ts +7 -0
- package/dist_ts/nosqldb/classes.cursoroperation.js +36 -0
- package/dist_ts/nosqldb/classes.db.d.ts +98 -0
- package/dist_ts/nosqldb/classes.db.js +434 -0
- package/dist_ts/nosqldb/classes.distributedcoordinator.d.ts +52 -0
- package/dist_ts/nosqldb/classes.distributedcoordinator.js +314 -0
- package/dist_ts/nosqldb/classes.doc.d.ts +722 -0
- package/dist_ts/nosqldb/classes.doc.js +2976 -0
- package/dist_ts/nosqldb/classes.easystore.d.ts +42 -0
- package/dist_ts/nosqldb/classes.easystore.js +199 -0
- package/dist_ts/nosqldb/classes.exactpersistence.d.ts +141 -0
- package/dist_ts/nosqldb/classes.exactpersistence.js +1110 -0
- package/dist_ts/nosqldb/classes.lucene.adapter.d.ts +173 -0
- package/dist_ts/nosqldb/classes.lucene.adapter.js +623 -0
- package/dist_ts/nosqldb/classes.mongoadministration.d.ts +73 -0
- package/dist_ts/nosqldb/classes.mongoadministration.js +497 -0
- package/dist_ts/nosqldb/classes.namespaceinspection.d.ts +42 -0
- package/dist_ts/nosqldb/classes.namespaceinspection.js +138 -0
- package/dist_ts/nosqldb/classes.operationbudget.d.ts +22 -0
- package/dist_ts/nosqldb/classes.operationbudget.js +65 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.d.ts +18 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.js +71 -0
- package/dist_ts/nosqldb/classes.persistence.d.ts +14 -0
- package/dist_ts/nosqldb/classes.persistence.js +34 -0
- package/dist_ts/nosqldb/classes.session.d.ts +28 -0
- package/dist_ts/nosqldb/classes.session.js +246 -0
- package/dist_ts/nosqldb/classes.storedinspection.d.ts +44 -0
- package/dist_ts/nosqldb/classes.storedinspection.js +100 -0
- package/dist_ts/nosqldb/classes.testdatabase.d.ts +56 -0
- package/dist_ts/nosqldb/classes.testdatabase.js +250 -0
- package/dist_ts/nosqldb/classes.watcher.d.ts +37 -0
- package/dist_ts/nosqldb/classes.watcher.js +98 -0
- package/dist_ts/nosqldb/index.d.ts +27 -0
- package/dist_ts/nosqldb/index.js +18 -0
- package/dist_ts/nosqldb/logging.d.ts +2 -0
- package/dist_ts/nosqldb/logging.js +3 -0
- package/dist_ts/nosqldb/plugins.d.ts +17 -0
- package/dist_ts/nosqldb/plugins.js +20 -0
- package/dist_ts/nosqldb/shim.d.ts +1 -0
- package/dist_ts/nosqldb/shim.js +8 -0
- package/dist_ts/nosqldb/testsupport.d.ts +3 -0
- package/dist_ts/nosqldb/testsupport.js +3 -0
- package/dist_ts/objectstorage/classes.bucket.d.ts +227 -0
- package/dist_ts/objectstorage/classes.bucket.js +939 -0
- package/dist_ts/objectstorage/classes.connection.d.ts +21 -0
- package/dist_ts/objectstorage/classes.connection.js +27 -0
- package/dist_ts/objectstorage/classes.directory.d.ts +124 -0
- package/dist_ts/objectstorage/classes.directory.js +335 -0
- package/dist_ts/objectstorage/classes.exactpathpurgeerror.d.ts +5 -0
- package/dist_ts/objectstorage/classes.exactpathpurgeerror.js +18 -0
- package/dist_ts/objectstorage/classes.exactuploaderror.d.ts +5 -0
- package/dist_ts/objectstorage/classes.exactuploaderror.js +19 -0
- package/dist_ts/objectstorage/classes.file.d.ts +91 -0
- package/dist_ts/objectstorage/classes.file.js +255 -0
- package/dist_ts/objectstorage/classes.listcursor.d.ts +41 -0
- package/dist_ts/objectstorage/classes.listcursor.js +67 -0
- package/dist_ts/objectstorage/classes.metadata.d.ts +57 -0
- package/dist_ts/objectstorage/classes.metadata.js +113 -0
- package/dist_ts/objectstorage/classes.smartbucket.d.ts +57 -0
- package/dist_ts/objectstorage/classes.smartbucket.js +357 -0
- package/dist_ts/objectstorage/classes.trash.d.ts +11 -0
- package/dist_ts/objectstorage/classes.trash.js +22 -0
- package/dist_ts/objectstorage/classes.watcher.d.ts +79 -0
- package/dist_ts/objectstorage/classes.watcher.js +249 -0
- package/dist_ts/objectstorage/helpers.d.ts +25 -0
- package/dist_ts/objectstorage/helpers.js +215 -0
- package/dist_ts/objectstorage/index.d.ts +13 -0
- package/dist_ts/objectstorage/index.js +13 -0
- package/dist_ts/objectstorage/interfaces.d.ts +276 -0
- package/dist_ts/objectstorage/interfaces.js +2 -0
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.d.ts +11 -0
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.js +1069 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.d.ts +40 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.js +382 -0
- package/dist_ts/objectstorage/internal.exactupload.operations.d.ts +11 -0
- package/dist_ts/objectstorage/internal.exactupload.operations.js +1104 -0
- package/dist_ts/objectstorage/internal.multipart.planner.d.ts +8 -0
- package/dist_ts/objectstorage/internal.multipart.planner.js +63 -0
- package/dist_ts/objectstorage/internal.multipart.stream.d.ts +17 -0
- package/dist_ts/objectstorage/internal.multipart.stream.js +440 -0
- package/dist_ts/objectstorage/internal.multipart.types.d.ts +82 -0
- package/dist_ts/objectstorage/internal.multipart.types.js +2 -0
- package/dist_ts/objectstorage/plugins.d.ts +17 -0
- package/dist_ts/objectstorage/plugins.js +23 -0
- package/dist_ts/plugins.d.ts +4 -0
- package/dist_ts/plugins.js +5 -0
- package/dist_ts/sqldb/classes.clickhouseconnection.d.ts +39 -0
- package/dist_ts/sqldb/classes.clickhouseconnection.js +225 -0
- package/dist_ts/sqldb/classes.sqlconnection.d.ts +50 -0
- package/dist_ts/sqldb/classes.sqlconnection.js +376 -0
- package/dist_ts/sqldb/classes.sqltable.d.ts +29 -0
- package/dist_ts/sqldb/classes.sqltable.js +70 -0
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.d.ts +27 -0
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.js +89 -0
- package/dist_ts/sqldb/clickhouse/index.d.ts +9 -0
- package/dist_ts/sqldb/clickhouse/index.js +13 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.d.ts +88 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.js +304 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.d.ts +28 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.js +46 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.d.ts +43 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.js +155 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.d.ts +13 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.js +36 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.d.ts +49 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.js +106 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.d.ts +51 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.js +96 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.d.ts +5 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.js +6 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.types.d.ts +53 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.types.js +41 -0
- package/dist_ts/sqldb/index.d.ts +6 -0
- package/dist_ts/sqldb/index.js +6 -0
- package/dist_ts/sqldb/interfaces.d.ts +53 -0
- package/dist_ts/sqldb/interfaces.js +19 -0
- package/dist_ts/sqldb/internal.sqlpool.d.ts +51 -0
- package/dist_ts/sqldb/internal.sqlpool.js +214 -0
- package/dist_ts/sqldb/plugins.clickhouse.d.ts +4 -0
- package/dist_ts/sqldb/plugins.clickhouse.js +5 -0
- package/dist_ts/sqldb/plugins.mariadb.d.ts +4 -0
- package/dist_ts/sqldb/plugins.mariadb.js +5 -0
- package/docs/source/smartbucket/license.md +19 -0
- package/docs/source/smartclickhouse/license.md +19 -0
- package/license.md +21 -0
- package/package.json +101 -0
- package/readme.md +157 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.losslessorgclient.ts +104 -0
- package/ts/core/classes.error.ts +31 -0
- package/ts/core/classes.operation.ts +60 -0
- package/ts/core/interfaces.ts +24 -0
- package/ts/index.ts +4 -0
- package/ts/nosqldb/classes.atomicdelete.ts +81 -0
- package/ts/nosqldb/classes.atomicfindoneandupdate.ts +152 -0
- package/ts/nosqldb/classes.atomicupdate.ts +99 -0
- package/ts/nosqldb/classes.bootstraprecord.ts +107 -0
- package/ts/nosqldb/classes.bsonassertion.ts +25 -0
- package/ts/nosqldb/classes.bsonobjectid.ts +45 -0
- package/ts/nosqldb/classes.bsonsnapshot.ts +105 -0
- package/ts/nosqldb/classes.cached-document.ts +67 -0
- package/ts/nosqldb/classes.collection.ts +2399 -0
- package/ts/nosqldb/classes.collectionfactory.ts +87 -0
- package/ts/nosqldb/classes.collectionlifecycle.ts +24 -0
- package/ts/nosqldb/classes.collectionpreparation.ts +164 -0
- package/ts/nosqldb/classes.collectiontopology.ts +645 -0
- package/ts/nosqldb/classes.connection.ts +31 -0
- package/ts/nosqldb/classes.convenience.ts +5 -0
- package/ts/nosqldb/classes.cursor.ts +88 -0
- package/ts/nosqldb/classes.cursoroperation.ts +38 -0
- package/ts/nosqldb/classes.db.ts +637 -0
- package/ts/nosqldb/classes.distributedcoordinator.ts +305 -0
- package/ts/nosqldb/classes.doc.ts +4928 -0
- package/ts/nosqldb/classes.easystore.ts +154 -0
- package/ts/nosqldb/classes.exactpersistence.ts +1974 -0
- package/ts/nosqldb/classes.lucene.adapter.ts +780 -0
- package/ts/nosqldb/classes.mongoadministration.ts +867 -0
- package/ts/nosqldb/classes.namespaceinspection.ts +199 -0
- package/ts/nosqldb/classes.operationbudget.ts +75 -0
- package/ts/nosqldb/classes.ordinarypersistence.ts +84 -0
- package/ts/nosqldb/classes.persistence.ts +62 -0
- package/ts/nosqldb/classes.session.ts +389 -0
- package/ts/nosqldb/classes.storedinspection.ts +151 -0
- package/ts/nosqldb/classes.testdatabase.ts +272 -0
- package/ts/nosqldb/classes.watcher.ts +96 -0
- package/ts/nosqldb/index.ts +51 -0
- package/ts/nosqldb/logging.ts +3 -0
- package/ts/nosqldb/plugins.ts +37 -0
- package/ts/nosqldb/shim.ts +6 -0
- package/ts/nosqldb/testsupport.ts +13 -0
- package/ts/objectstorage/classes.bucket.ts +1114 -0
- package/ts/objectstorage/classes.connection.ts +36 -0
- package/ts/objectstorage/classes.directory.ts +425 -0
- package/ts/objectstorage/classes.exactpathpurgeerror.ts +24 -0
- package/ts/objectstorage/classes.exactuploaderror.ts +25 -0
- package/ts/objectstorage/classes.file.ts +303 -0
- package/ts/objectstorage/classes.listcursor.ts +89 -0
- package/ts/objectstorage/classes.metadata.ts +134 -0
- package/ts/objectstorage/classes.smartbucket.ts +445 -0
- package/ts/objectstorage/classes.trash.ts +30 -0
- package/ts/objectstorage/classes.watcher.ts +289 -0
- package/ts/objectstorage/helpers.ts +254 -0
- package/ts/objectstorage/index.ts +13 -0
- package/ts/objectstorage/interfaces.ts +389 -0
- package/ts/objectstorage/internal.exactpathpurge.operations.ts +1936 -0
- package/ts/objectstorage/internal.exactupload.capability.ts +580 -0
- package/ts/objectstorage/internal.exactupload.operations.ts +1715 -0
- package/ts/objectstorage/internal.multipart.planner.ts +90 -0
- package/ts/objectstorage/internal.multipart.stream.ts +606 -0
- package/ts/objectstorage/internal.multipart.types.ts +106 -0
- package/ts/objectstorage/plugins.ts +35 -0
- package/ts/plugins.ts +4 -0
- package/ts/sqldb/classes.clickhouseconnection.ts +189 -0
- package/ts/sqldb/classes.sqlconnection.ts +350 -0
- package/ts/sqldb/classes.sqltable.ts +70 -0
- package/ts/sqldb/clickhouse/classes.metricwriter.ts +72 -0
- package/ts/sqldb/clickhouse/index.ts +16 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.ts +372 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.ts +52 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.ts +120 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.resultset.ts +44 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.ts +123 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.ts +114 -0
- package/ts/sqldb/clickhouse/smartclickhouse.plugins.ts +5 -0
- package/ts/sqldb/clickhouse/smartclickhouse.types.ts +134 -0
- package/ts/sqldb/index.ts +6 -0
- package/ts/sqldb/interfaces.ts +61 -0
- package/ts/sqldb/internal.sqlpool.ts +179 -0
- package/ts/sqldb/plugins.clickhouse.ts +4 -0
- package/ts/sqldb/plugins.mariadb.ts +4 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// classes.watcher.ts
|
|
2
|
+
|
|
3
|
+
import * as plugins from './plugins.js';
|
|
4
|
+
import * as interfaces from './interfaces.js';
|
|
5
|
+
import type { Bucket } from './classes.bucket.js';
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* BucketWatcher monitors a storage bucket for changes (add/modify/delete)
|
|
10
|
+
* using a polling-based approach. Designed to follow the SmartdataDbWatcher pattern.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const watcher = bucket.createWatcher({ prefix: 'uploads/', pollIntervalMs: 3000 });
|
|
15
|
+
*
|
|
16
|
+
* // RxJS Observable pattern
|
|
17
|
+
* watcher.changeSubject.subscribe((change) => {
|
|
18
|
+
* console.log('Change:', change);
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // EventEmitter pattern
|
|
22
|
+
* watcher.on('change', (change) => console.log(change));
|
|
23
|
+
* watcher.on('error', (err) => console.error(err));
|
|
24
|
+
*
|
|
25
|
+
* await watcher.start();
|
|
26
|
+
* await watcher.readyDeferred.promise; // Wait for initial state
|
|
27
|
+
*
|
|
28
|
+
* // Later...
|
|
29
|
+
* await watcher.stop();
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class BucketWatcher extends EventEmitter {
|
|
33
|
+
/** Deferred that resolves when initial state is built and watcher is ready */
|
|
34
|
+
public readyDeferred = plugins.smartpromise.defer();
|
|
35
|
+
|
|
36
|
+
/** Observable for receiving change events (supports RxJS operators) */
|
|
37
|
+
public changeSubject: plugins.smartrx.rxjs.Observable<interfaces.IStorageChangeEvent | interfaces.IStorageChangeEvent[]>;
|
|
38
|
+
|
|
39
|
+
// Internal subjects and state
|
|
40
|
+
private rawSubject: plugins.smartrx.rxjs.Subject<interfaces.IStorageChangeEvent>;
|
|
41
|
+
private previousState: Map<string, interfaces.IStorageObjectState>;
|
|
42
|
+
private pollIntervalId: ReturnType<typeof setInterval> | null = null;
|
|
43
|
+
private isPolling = false;
|
|
44
|
+
private isStopped = false;
|
|
45
|
+
|
|
46
|
+
// Configuration
|
|
47
|
+
private readonly bucketRef: Bucket;
|
|
48
|
+
private readonly prefix: string;
|
|
49
|
+
private readonly pollIntervalMs: number;
|
|
50
|
+
private readonly bufferTimeMs?: number;
|
|
51
|
+
private readonly includeInitial: boolean;
|
|
52
|
+
private readonly pageSize: number;
|
|
53
|
+
|
|
54
|
+
constructor(bucketRef: Bucket, options: interfaces.IBucketWatcherOptions = {}) {
|
|
55
|
+
super();
|
|
56
|
+
|
|
57
|
+
this.bucketRef = bucketRef;
|
|
58
|
+
this.prefix = options.prefix ?? '';
|
|
59
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 5000;
|
|
60
|
+
this.bufferTimeMs = options.bufferTimeMs;
|
|
61
|
+
this.includeInitial = options.includeInitial ?? false;
|
|
62
|
+
this.pageSize = options.pageSize ?? 1000;
|
|
63
|
+
|
|
64
|
+
// Initialize state tracking
|
|
65
|
+
this.previousState = new Map();
|
|
66
|
+
|
|
67
|
+
// Initialize raw subject for emitting changes
|
|
68
|
+
this.rawSubject = new plugins.smartrx.rxjs.Subject<interfaces.IStorageChangeEvent>();
|
|
69
|
+
|
|
70
|
+
// Configure the public observable with optional buffering
|
|
71
|
+
if (this.bufferTimeMs && this.bufferTimeMs > 0) {
|
|
72
|
+
this.changeSubject = this.rawSubject.pipe(
|
|
73
|
+
plugins.smartrx.rxjs.ops.bufferTime(this.bufferTimeMs),
|
|
74
|
+
plugins.smartrx.rxjs.ops.filter((events: interfaces.IStorageChangeEvent[]) => events.length > 0)
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
this.changeSubject = this.rawSubject.asObservable();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Start watching the bucket for changes
|
|
83
|
+
*/
|
|
84
|
+
public async start(): Promise<void> {
|
|
85
|
+
if (this.pollIntervalId !== null) {
|
|
86
|
+
console.log('BucketWatcher is already running');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.isStopped = false;
|
|
91
|
+
|
|
92
|
+
// Build initial state
|
|
93
|
+
await this.buildInitialState();
|
|
94
|
+
|
|
95
|
+
// Emit initial state as 'add' events if configured
|
|
96
|
+
if (this.includeInitial) {
|
|
97
|
+
for (const state of this.previousState.values()) {
|
|
98
|
+
this.emitChange({
|
|
99
|
+
type: 'add',
|
|
100
|
+
key: state.key,
|
|
101
|
+
size: state.size,
|
|
102
|
+
etag: state.etag,
|
|
103
|
+
lastModified: state.lastModified,
|
|
104
|
+
bucket: this.bucketRef.name,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Mark as ready
|
|
110
|
+
this.readyDeferred.resolve();
|
|
111
|
+
|
|
112
|
+
// Start polling loop
|
|
113
|
+
this.pollIntervalId = setInterval(() => {
|
|
114
|
+
this.poll().catch((err) => {
|
|
115
|
+
this.emit('error', err);
|
|
116
|
+
});
|
|
117
|
+
}, this.pollIntervalMs);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Stop watching the bucket
|
|
122
|
+
*/
|
|
123
|
+
public async stop(): Promise<void> {
|
|
124
|
+
this.isStopped = true;
|
|
125
|
+
|
|
126
|
+
if (this.pollIntervalId !== null) {
|
|
127
|
+
clearInterval(this.pollIntervalId);
|
|
128
|
+
this.pollIntervalId = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Wait for any in-progress poll to complete
|
|
132
|
+
while (this.isPolling) {
|
|
133
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 50));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.rawSubject.complete();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Alias for stop() - for consistency with other APIs
|
|
141
|
+
*/
|
|
142
|
+
public async close(): Promise<void> {
|
|
143
|
+
return this.stop();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Build the initial state by listing all objects with metadata
|
|
148
|
+
*/
|
|
149
|
+
private async buildInitialState(): Promise<void> {
|
|
150
|
+
this.previousState.clear();
|
|
151
|
+
|
|
152
|
+
for await (const obj of this.listObjectsWithMetadata()) {
|
|
153
|
+
if (obj.Key) {
|
|
154
|
+
this.previousState.set(obj.Key, {
|
|
155
|
+
key: obj.Key,
|
|
156
|
+
etag: obj.ETag ?? '',
|
|
157
|
+
size: obj.Size ?? 0,
|
|
158
|
+
lastModified: obj.LastModified ?? new Date(0),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Poll for changes by comparing current state against previous state
|
|
166
|
+
*/
|
|
167
|
+
private async poll(): Promise<void> {
|
|
168
|
+
// Guard against overlapping polls
|
|
169
|
+
if (this.isPolling || this.isStopped) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.isPolling = true;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
// Build current state
|
|
177
|
+
const currentState = new Map<string, interfaces.IStorageObjectState>();
|
|
178
|
+
|
|
179
|
+
for await (const obj of this.listObjectsWithMetadata()) {
|
|
180
|
+
if (this.isStopped) {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (obj.Key) {
|
|
185
|
+
currentState.set(obj.Key, {
|
|
186
|
+
key: obj.Key,
|
|
187
|
+
etag: obj.ETag ?? '',
|
|
188
|
+
size: obj.Size ?? 0,
|
|
189
|
+
lastModified: obj.LastModified ?? new Date(0),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!this.isStopped) {
|
|
195
|
+
this.detectChanges(currentState);
|
|
196
|
+
this.previousState = currentState;
|
|
197
|
+
}
|
|
198
|
+
} catch (err) {
|
|
199
|
+
this.emit('error', err);
|
|
200
|
+
} finally {
|
|
201
|
+
this.isPolling = false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Detect changes between current and previous state
|
|
207
|
+
*/
|
|
208
|
+
private detectChanges(currentState: Map<string, interfaces.IStorageObjectState>): void {
|
|
209
|
+
// Detect added and modified objects
|
|
210
|
+
for (const [key, current] of currentState) {
|
|
211
|
+
const previous = this.previousState.get(key);
|
|
212
|
+
|
|
213
|
+
if (!previous) {
|
|
214
|
+
// New object - emit 'add' event
|
|
215
|
+
this.emitChange({
|
|
216
|
+
type: 'add',
|
|
217
|
+
key: current.key,
|
|
218
|
+
size: current.size,
|
|
219
|
+
etag: current.etag,
|
|
220
|
+
lastModified: current.lastModified,
|
|
221
|
+
bucket: this.bucketRef.name,
|
|
222
|
+
});
|
|
223
|
+
} else if (
|
|
224
|
+
previous.etag !== current.etag ||
|
|
225
|
+
previous.size !== current.size ||
|
|
226
|
+
previous.lastModified.getTime() !== current.lastModified.getTime()
|
|
227
|
+
) {
|
|
228
|
+
// Object modified - emit 'modify' event
|
|
229
|
+
this.emitChange({
|
|
230
|
+
type: 'modify',
|
|
231
|
+
key: current.key,
|
|
232
|
+
size: current.size,
|
|
233
|
+
etag: current.etag,
|
|
234
|
+
lastModified: current.lastModified,
|
|
235
|
+
bucket: this.bucketRef.name,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Detect deleted objects
|
|
241
|
+
for (const [key, previous] of this.previousState) {
|
|
242
|
+
if (!currentState.has(key)) {
|
|
243
|
+
// Object deleted - emit 'delete' event
|
|
244
|
+
this.emitChange({
|
|
245
|
+
type: 'delete',
|
|
246
|
+
key: previous.key,
|
|
247
|
+
bucket: this.bucketRef.name,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Emit a change event via both RxJS Subject and EventEmitter
|
|
255
|
+
*/
|
|
256
|
+
private emitChange(event: interfaces.IStorageChangeEvent): void {
|
|
257
|
+
this.rawSubject.next(event);
|
|
258
|
+
this.emit('change', event);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* List objects with full metadata (ETag, Size, LastModified)
|
|
263
|
+
* This is a private method that yields full _Object data, not just keys
|
|
264
|
+
*/
|
|
265
|
+
private async *listObjectsWithMetadata(): AsyncIterableIterator<plugins.s3._Object> {
|
|
266
|
+
let continuationToken: string | undefined;
|
|
267
|
+
|
|
268
|
+
do {
|
|
269
|
+
if (this.isStopped) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const command = new plugins.s3.ListObjectsV2Command({
|
|
274
|
+
Bucket: this.bucketRef.name,
|
|
275
|
+
Prefix: this.prefix,
|
|
276
|
+
MaxKeys: this.pageSize,
|
|
277
|
+
ContinuationToken: continuationToken,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const response = await this.bucketRef.smartbucketRef.storageClient.send(command);
|
|
281
|
+
|
|
282
|
+
for (const obj of response.Contents || []) {
|
|
283
|
+
yield obj;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
continuationToken = response.NextContinuationToken;
|
|
287
|
+
} while (continuationToken);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import * as interfaces from './interfaces.js';
|
|
3
|
+
|
|
4
|
+
export const reducePathDescriptorToPath = async (pathDescriptorArg: interfaces.IPathDecriptor): Promise<string> => {
|
|
5
|
+
let returnPath = ``
|
|
6
|
+
if (pathDescriptorArg.directory) {
|
|
7
|
+
if (pathDescriptorArg.path && plugins.path.isAbsolute(pathDescriptorArg.path)) {
|
|
8
|
+
console.warn('Directory is being ignored when path is absolute.');
|
|
9
|
+
returnPath = pathDescriptorArg.path;
|
|
10
|
+
} else if (pathDescriptorArg.path) {
|
|
11
|
+
returnPath = plugins.path.join(pathDescriptorArg.directory.getBasePath(), pathDescriptorArg.path);
|
|
12
|
+
}
|
|
13
|
+
} else if (pathDescriptorArg.path) {
|
|
14
|
+
returnPath = pathDescriptorArg.path;
|
|
15
|
+
} else {
|
|
16
|
+
throw new Error('You must specify either a path or a directory.');
|
|
17
|
+
}
|
|
18
|
+
if (returnPath.startsWith('/')) {
|
|
19
|
+
returnPath = returnPath.substring(1);
|
|
20
|
+
}
|
|
21
|
+
return returnPath;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Storage Descriptor Normalization
|
|
25
|
+
export interface IStorageWarning {
|
|
26
|
+
code: string;
|
|
27
|
+
message: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface INormalizedStorageConfig {
|
|
31
|
+
endpointUrl: string;
|
|
32
|
+
host: string;
|
|
33
|
+
protocol: 'http' | 'https';
|
|
34
|
+
port?: number;
|
|
35
|
+
region: string;
|
|
36
|
+
credentials: {
|
|
37
|
+
accessKeyId: string;
|
|
38
|
+
secretAccessKey: string;
|
|
39
|
+
};
|
|
40
|
+
forcePathStyle: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function coerceBooleanMaybe(value: unknown): { value: boolean | undefined; warning?: IStorageWarning } {
|
|
44
|
+
if (typeof value === 'boolean') return { value };
|
|
45
|
+
if (typeof value === 'string') {
|
|
46
|
+
const v = value.trim().toLowerCase();
|
|
47
|
+
if (v === 'true' || v === '1') {
|
|
48
|
+
return {
|
|
49
|
+
value: true,
|
|
50
|
+
warning: {
|
|
51
|
+
code: 'SBK_S3_COERCED_USESSL',
|
|
52
|
+
message: `Coerced useSsl='${value}' (string) to boolean true.`
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (v === 'false' || v === '0') {
|
|
57
|
+
return {
|
|
58
|
+
value: false,
|
|
59
|
+
warning: {
|
|
60
|
+
code: 'SBK_S3_COERCED_USESSL',
|
|
61
|
+
message: `Coerced useSsl='${value}' (string) to boolean false.`
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { value: undefined };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function coercePortMaybe(port: unknown): { value: number | undefined; warning?: IStorageWarning } {
|
|
70
|
+
if (port === undefined || port === null || port === '') return { value: undefined };
|
|
71
|
+
const n = typeof port === 'number' ? port : Number(String(port).trim());
|
|
72
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0 || n > 65535) {
|
|
73
|
+
return {
|
|
74
|
+
value: undefined,
|
|
75
|
+
warning: {
|
|
76
|
+
code: 'SBK_S3_INVALID_PORT',
|
|
77
|
+
message: `Invalid port '${String(port)}' - expected integer in [1..65535].`
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { value: n };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sanitizeEndpointString(raw: unknown): { value: string; warnings: IStorageWarning[] } {
|
|
85
|
+
const warnings: IStorageWarning[] = [];
|
|
86
|
+
let s = String(raw ?? '').trim();
|
|
87
|
+
if (s !== String(raw ?? '')) {
|
|
88
|
+
warnings.push({
|
|
89
|
+
code: 'SBK_S3_TRIMMED_ENDPOINT',
|
|
90
|
+
message: 'Trimmed surrounding whitespace from endpoint.'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return { value: s, warnings };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseEndpointHostPort(
|
|
97
|
+
endpoint: string,
|
|
98
|
+
provisionalProtocol: 'http' | 'https'
|
|
99
|
+
): {
|
|
100
|
+
hadScheme: boolean;
|
|
101
|
+
host: string;
|
|
102
|
+
port?: number;
|
|
103
|
+
extras: {
|
|
104
|
+
droppedPath?: boolean;
|
|
105
|
+
droppedQuery?: boolean;
|
|
106
|
+
droppedCreds?: boolean
|
|
107
|
+
}
|
|
108
|
+
} {
|
|
109
|
+
let url: URL | undefined;
|
|
110
|
+
const extras: { droppedPath?: boolean; droppedQuery?: boolean; droppedCreds?: boolean } = {};
|
|
111
|
+
|
|
112
|
+
// Check if endpoint already has a scheme
|
|
113
|
+
const hasScheme = /^https?:\/\//i.test(endpoint);
|
|
114
|
+
|
|
115
|
+
// Try parsing as full URL first
|
|
116
|
+
try {
|
|
117
|
+
if (hasScheme) {
|
|
118
|
+
url = new URL(endpoint);
|
|
119
|
+
} else {
|
|
120
|
+
// Not a full URL; try host[:port] by attaching provisional scheme
|
|
121
|
+
// Remove anything after first '/' for safety
|
|
122
|
+
const cleanEndpoint = endpoint.replace(/\/.*/, '');
|
|
123
|
+
url = new URL(`${provisionalProtocol}://${cleanEndpoint}`);
|
|
124
|
+
}
|
|
125
|
+
} catch (e) {
|
|
126
|
+
throw new Error(`Unable to parse endpoint '${endpoint}'.`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check for dropped components
|
|
130
|
+
if (url.username || url.password) extras.droppedCreds = true;
|
|
131
|
+
if (url.pathname && url.pathname !== '/') extras.droppedPath = true;
|
|
132
|
+
if (url.search) extras.droppedQuery = true;
|
|
133
|
+
|
|
134
|
+
const hadScheme = hasScheme;
|
|
135
|
+
const host = url.hostname; // hostnames lowercased by URL; IPs preserved
|
|
136
|
+
const port = url.port ? Number(url.port) : undefined;
|
|
137
|
+
|
|
138
|
+
return { hadScheme, host, port, extras };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function normalizeStorageDescriptor(
|
|
142
|
+
input: plugins.tsclass.storage.IStorageDescriptor,
|
|
143
|
+
logger?: { warn: (msg: string) => void }
|
|
144
|
+
): { normalized: INormalizedStorageConfig; warnings: IStorageWarning[] } {
|
|
145
|
+
const warnings: IStorageWarning[] = [];
|
|
146
|
+
const logWarn = (w: IStorageWarning) => {
|
|
147
|
+
warnings.push(w);
|
|
148
|
+
if (logger) {
|
|
149
|
+
logger.warn(`[SmartBucket] ${w.code}: ${w.message}`);
|
|
150
|
+
} else {
|
|
151
|
+
console.warn(`[SmartBucket] ${w.code}: ${w.message}`);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Coerce and sanitize inputs
|
|
156
|
+
const { value: coercedUseSsl, warning: useSslWarn } = coerceBooleanMaybe((input as any).useSsl);
|
|
157
|
+
if (useSslWarn) logWarn(useSslWarn);
|
|
158
|
+
|
|
159
|
+
const { value: coercedPort, warning: portWarn } = coercePortMaybe((input as any).port);
|
|
160
|
+
if (portWarn) logWarn(portWarn);
|
|
161
|
+
|
|
162
|
+
const { value: endpointStr, warnings: endpointSanWarnings } = sanitizeEndpointString((input as any).endpoint);
|
|
163
|
+
endpointSanWarnings.forEach(logWarn);
|
|
164
|
+
|
|
165
|
+
if (!endpointStr) {
|
|
166
|
+
throw new Error('Storage endpoint is required (got empty string). Provide hostname or URL.');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Provisional protocol selection for parsing host:port forms
|
|
170
|
+
const provisionalProtocol: 'http' | 'https' = coercedUseSsl === false ? 'http' : 'https';
|
|
171
|
+
|
|
172
|
+
const { hadScheme, host, port: epPort, extras } = parseEndpointHostPort(endpointStr, provisionalProtocol);
|
|
173
|
+
|
|
174
|
+
if (extras.droppedCreds) {
|
|
175
|
+
logWarn({
|
|
176
|
+
code: 'SBK_S3_DROPPED_CREDENTIALS',
|
|
177
|
+
message: 'Ignored credentials in endpoint URL.'
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (extras.droppedPath) {
|
|
181
|
+
logWarn({
|
|
182
|
+
code: 'SBK_S3_DROPPED_PATH',
|
|
183
|
+
message: 'Removed path segment from endpoint URL; S3 endpoint should be host[:port] only.'
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (extras.droppedQuery) {
|
|
187
|
+
logWarn({
|
|
188
|
+
code: 'SBK_S3_DROPPED_QUERY',
|
|
189
|
+
message: 'Removed query string from endpoint URL; S3 endpoint should be host[:port] only.'
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Final protocol decision
|
|
194
|
+
let finalProtocol: 'http' | 'https';
|
|
195
|
+
if (hadScheme) {
|
|
196
|
+
// Scheme from endpoint wins
|
|
197
|
+
const schemeFromEndpoint = endpointStr.trim().toLowerCase().startsWith('http://') ? 'http' : 'https';
|
|
198
|
+
finalProtocol = schemeFromEndpoint;
|
|
199
|
+
if (typeof coercedUseSsl === 'boolean') {
|
|
200
|
+
const expected = coercedUseSsl ? 'https' : 'http';
|
|
201
|
+
if (expected !== finalProtocol) {
|
|
202
|
+
logWarn({
|
|
203
|
+
code: 'SBK_S3_SCHEME_CONFLICT',
|
|
204
|
+
message: `useSsl=${String(coercedUseSsl)} conflicts with endpoint scheme '${finalProtocol}'; using endpoint scheme.`
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
if (typeof coercedUseSsl === 'boolean') {
|
|
210
|
+
finalProtocol = coercedUseSsl ? 'https' : 'http';
|
|
211
|
+
} else {
|
|
212
|
+
finalProtocol = 'https';
|
|
213
|
+
logWarn({
|
|
214
|
+
code: 'SBK_S3_GUESSED_PROTOCOL',
|
|
215
|
+
message: "No scheme in endpoint and useSsl not provided; defaulting to 'https'."
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Final port decision
|
|
221
|
+
let finalPort: number | undefined = undefined;
|
|
222
|
+
if (coercedPort !== undefined && epPort !== undefined && coercedPort !== epPort) {
|
|
223
|
+
logWarn({
|
|
224
|
+
code: 'SBK_S3_PORT_CONFLICT',
|
|
225
|
+
message: `Port in config (${coercedPort}) conflicts with endpoint port (${epPort}); using config port.`
|
|
226
|
+
});
|
|
227
|
+
finalPort = coercedPort;
|
|
228
|
+
} else {
|
|
229
|
+
finalPort = (coercedPort !== undefined) ? coercedPort : epPort;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Build canonical endpoint URL (origin only, no trailing slash)
|
|
233
|
+
const url = new URL(`${finalProtocol}://${host}`);
|
|
234
|
+
if (finalPort !== undefined) url.port = String(finalPort);
|
|
235
|
+
const endpointUrl = url.origin;
|
|
236
|
+
|
|
237
|
+
const region = input.region || 'us-east-1';
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
normalized: {
|
|
241
|
+
endpointUrl,
|
|
242
|
+
host,
|
|
243
|
+
protocol: finalProtocol,
|
|
244
|
+
port: finalPort,
|
|
245
|
+
region,
|
|
246
|
+
credentials: {
|
|
247
|
+
accessKeyId: input.accessKey,
|
|
248
|
+
secretAccessKey: input.accessSecret,
|
|
249
|
+
},
|
|
250
|
+
forcePathStyle: true,
|
|
251
|
+
},
|
|
252
|
+
warnings,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './classes.smartbucket.js';
|
|
2
|
+
export * from './classes.bucket.js';
|
|
3
|
+
export * from './classes.exactuploaderror.js';
|
|
4
|
+
export * from './classes.exactpathpurgeerror.js';
|
|
5
|
+
export * from './classes.directory.js';
|
|
6
|
+
export * from './classes.file.js';
|
|
7
|
+
export * from './classes.listcursor.js';
|
|
8
|
+
export * from './classes.metadata.js';
|
|
9
|
+
export * from './classes.trash.js';
|
|
10
|
+
export * from './classes.watcher.js';
|
|
11
|
+
export * from './interfaces.js';
|
|
12
|
+
export { ObjectStorageConnection } from './classes.connection.js';
|
|
13
|
+
export type { IObjectStorageConnectionOptions } from './classes.connection.js';
|