@lossless.org/client 1.7.0 → 1.8.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.
Files changed (51) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/nosqldb/classes.atomicdelete.d.ts +1 -0
  3. package/dist_ts/nosqldb/classes.atomicdelete.js +10 -7
  4. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +2 -0
  5. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +9 -5
  6. package/dist_ts/nosqldb/classes.atomicupdate.d.ts +2 -0
  7. package/dist_ts/nosqldb/classes.atomicupdate.js +13 -8
  8. package/dist_ts/nosqldb/classes.collection.d.ts +13 -4
  9. package/dist_ts/nosqldb/classes.collection.js +57 -64
  10. package/dist_ts/nosqldb/classes.db.d.ts +1 -1
  11. package/dist_ts/nosqldb/classes.doc.d.ts +60 -3
  12. package/dist_ts/nosqldb/classes.doc.js +68 -26
  13. package/dist_ts/nosqldb/classes.exactpersistence.d.ts +36 -7
  14. package/dist_ts/nosqldb/classes.exactpersistence.js +92 -81
  15. package/dist_ts/nosqldb/classes.operationdeadline.d.ts +61 -0
  16. package/dist_ts/nosqldb/classes.operationdeadline.js +149 -0
  17. package/dist_ts/nosqldb/classes.persistence.d.ts +16 -1
  18. package/dist_ts/nosqldb/classes.persistence.js +20 -1
  19. package/dist_ts/nosqldb/classes.session.d.ts +73 -2
  20. package/dist_ts/nosqldb/classes.session.js +164 -38
  21. package/dist_ts/objectstorage/classes.bucket.d.ts +8 -3
  22. package/dist_ts/objectstorage/classes.bucket.js +83 -131
  23. package/dist_ts/objectstorage/classes.directory.js +17 -28
  24. package/dist_ts/objectstorage/classes.smartbucket.d.ts +13 -6
  25. package/dist_ts/objectstorage/classes.smartbucket.js +18 -12
  26. package/dist_ts/objectstorage/classes.watcher.js +1 -2
  27. package/dist_ts/objectstorage/interfaces.d.ts +8 -0
  28. package/dist_ts/objectstorage/internal.bucketrequests.d.ts +14 -0
  29. package/dist_ts/objectstorage/internal.bucketrequests.js +53 -0
  30. package/dist_ts/objectstorage/internal.exactupload.capability.d.ts +12 -0
  31. package/dist_ts/objectstorage/internal.exactupload.capability.js +46 -17
  32. package/package.json +1 -1
  33. package/readme.md +6 -1
  34. package/ts/00_commitinfo_data.ts +1 -1
  35. package/ts/nosqldb/classes.atomicdelete.ts +10 -6
  36. package/ts/nosqldb/classes.atomicfindoneandupdate.ts +10 -4
  37. package/ts/nosqldb/classes.atomicupdate.ts +14 -7
  38. package/ts/nosqldb/classes.collection.ts +82 -73
  39. package/ts/nosqldb/classes.db.ts +1 -1
  40. package/ts/nosqldb/classes.doc.ts +162 -33
  41. package/ts/nosqldb/classes.exactpersistence.ts +131 -93
  42. package/ts/nosqldb/classes.operationdeadline.ts +179 -0
  43. package/ts/nosqldb/classes.persistence.ts +36 -1
  44. package/ts/nosqldb/classes.session.ts +221 -39
  45. package/ts/objectstorage/classes.bucket.ts +111 -158
  46. package/ts/objectstorage/classes.directory.ts +18 -27
  47. package/ts/objectstorage/classes.smartbucket.ts +21 -11
  48. package/ts/objectstorage/classes.watcher.ts +0 -1
  49. package/ts/objectstorage/interfaces.ts +9 -0
  50. package/ts/objectstorage/internal.bucketrequests.ts +70 -0
  51. package/ts/objectstorage/internal.exactupload.capability.ts +105 -24
@@ -18,6 +18,7 @@ import {
18
18
  purgeExactPath as purgeExactPathOperation,
19
19
  } from './internal.exactpathpurge.operations.js';
20
20
  import { beginBasicBucketOperation } from './internal.exactupload.capability.js';
21
+ import { headBucketExists, runAccountRequest } from './internal.bucketrequests.js';
21
22
 
22
23
  export interface IListObjectKeysPageOptions {
23
24
  prefix?: string;
@@ -240,29 +241,45 @@ const destroyReadableAndWaitForSettlement = async (
240
241
  * operate on blobs of data in an S3-compatible object store.
241
242
  */
242
243
  export class Bucket {
243
- public static async getBucketByName(smartbucketRef: SmartBucket, bucketNameArg: string): Promise<Bucket> {
244
- const command = new plugins.s3.ListBucketsCommand({});
245
- const buckets = await smartbucketRef.storageClient.send(command);
246
- const foundBucket = buckets.Buckets!.find((bucket) => bucket.Name === bucketNameArg);
247
-
248
- if (foundBucket) {
249
- console.log(`bucket with name ${bucketNameArg} exists.`);
250
- console.log(`Taking this as base for new Bucket instance`);
251
- return new this(smartbucketRef, bucketNameArg);
252
- } else {
244
+ /**
245
+ * Returns a reference to an existing bucket, checked with one HeadBucket
246
+ * request. Rejects when the bucket does not exist or the credentials may
247
+ * not access it.
248
+ */
249
+ public static async getBucketByName(
250
+ smartbucketRef: SmartBucket,
251
+ bucketNameArg: string,
252
+ optionsArg: interfaces.IBucketRequestOptions = {},
253
+ ): Promise<Bucket> {
254
+ if (!(await headBucketExists(smartbucketRef, bucketNameArg, optionsArg.signal))) {
253
255
  throw new Error(`Bucket '${bucketNameArg}' not found.`);
254
256
  }
257
+ return new this(smartbucketRef, bucketNameArg);
255
258
  }
256
259
 
257
- public static async createBucketByName(smartbucketRef: SmartBucket, bucketName: string) {
258
- const command = new plugins.s3.CreateBucketCommand({ Bucket: bucketName });
259
- await smartbucketRef.storageClient.send(command);
260
+ public static async createBucketByName(
261
+ smartbucketRef: SmartBucket,
262
+ bucketName: string,
263
+ optionsArg: interfaces.IBucketRequestOptions = {},
264
+ ) {
265
+ await runAccountRequest(smartbucketRef, optionsArg.signal, (clientArg, abortSignalArg) =>
266
+ clientArg.send(new plugins.s3.CreateBucketCommand({ Bucket: bucketName }), {
267
+ abortSignal: abortSignalArg,
268
+ }),
269
+ );
260
270
  return new Bucket(smartbucketRef, bucketName);
261
271
  }
262
272
 
263
- public static async removeBucketByName(smartbucketRef: SmartBucket, bucketName: string) {
264
- const command = new plugins.s3.DeleteBucketCommand({ Bucket: bucketName });
265
- await smartbucketRef.storageClient.send(command);
273
+ public static async removeBucketByName(
274
+ smartbucketRef: SmartBucket,
275
+ bucketName: string,
276
+ optionsArg: interfaces.IBucketRequestOptions = {},
277
+ ) {
278
+ await runAccountRequest(smartbucketRef, optionsArg.signal, (clientArg, abortSignalArg) =>
279
+ clientArg.send(new plugins.s3.DeleteBucketCommand({ Bucket: bucketName }), {
280
+ abortSignal: abortSignalArg,
281
+ }),
282
+ );
266
283
  }
267
284
 
268
285
  public smartbucketRef: SmartBucket;
@@ -353,43 +370,32 @@ export class Bucket {
353
370
  overwrite?: boolean;
354
371
  }
355
372
  ): Promise<File> {
356
- try {
357
- const reducedPath = await helpers.reducePathDescriptorToPath(optionsArg);
358
- if (optionsArg.overwrite !== true) {
359
- const exists = await this.fastExists({ path: reducedPath });
360
-
361
- if (exists) {
362
- throw new Error(
363
- `Object already exists at path '${reducedPath}' in bucket '${this.name}'. ` +
364
- `Set overwrite:true to replace it.`
365
- );
366
- } else {
367
- console.log(`Creating new object at path '${reducedPath}' in bucket '${this.name}'.`);
368
- }
373
+ const reducedPath = await helpers.reducePathDescriptorToPath(optionsArg);
374
+ if (optionsArg.overwrite !== true) {
375
+ const exists = await this.fastExists({ path: reducedPath });
376
+
377
+ if (exists) {
378
+ throw new Error(
379
+ `Object already exists at path '${reducedPath}' in bucket '${this.name}'. ` +
380
+ `Set overwrite:true to replace it.`
381
+ );
369
382
  }
370
-
371
- const command = new plugins.s3.PutObjectCommand({
372
- Bucket: this.name,
373
- Key: reducedPath,
374
- Body: optionsArg.contents,
375
- });
376
- await this.smartbucketRef.storageClient.send(command);
377
-
378
- console.log(`Object '${reducedPath}' has been successfully stored in bucket '${this.name}'.`);
379
- const parsedPath = plugins.path.parse(reducedPath);
380
- return new File({
381
- directoryRefArg: await this.getDirectoryFromPath({
382
- path: parsedPath.dir,
383
- }),
384
- fileName: parsedPath.base,
385
- });
386
- } catch (error) {
387
- console.error(
388
- `Error storing object at path '${optionsArg.path}' in bucket '${this.name}':`,
389
- error
390
- );
391
- throw error;
392
383
  }
384
+
385
+ const command = new plugins.s3.PutObjectCommand({
386
+ Bucket: this.name,
387
+ Key: reducedPath,
388
+ Body: optionsArg.contents,
389
+ });
390
+ await this.smartbucketRef.storageClient.send(command);
391
+
392
+ const parsedPath = plugins.path.parse(reducedPath);
393
+ return new File({
394
+ directoryRefArg: await this.getDirectoryFromPath({
395
+ path: parsedPath.dir,
396
+ }),
397
+ fileName: parsedPath.base,
398
+ });
393
399
  }
394
400
 
395
401
 
@@ -714,8 +720,6 @@ export class Bucket {
714
720
  `Object already exists at path '${optionsArg.path}' in bucket '${this.name}'. ` +
715
721
  `Set overwrite:true to replace it.`
716
722
  );
717
- } else {
718
- console.log(`Creating new object at path '${optionsArg.path}' in bucket '${this.name}'.`);
719
723
  }
720
724
  }
721
725
 
@@ -734,18 +738,10 @@ export class Bucket {
734
738
  await operation.runProvider(
735
739
  () => operation.client.send(command, { abortSignal: operation.signal }),
736
740
  );
737
-
738
- console.log(
739
- `Object '${optionsArg.path}' has been successfully stored in bucket '${this.name}'.`
740
- );
741
741
  } catch (error) {
742
742
  if (sourceOwnershipStarted || putDispatched || operation.signal.aborted) {
743
743
  await disposeSource();
744
744
  }
745
- console.error(
746
- `Error storing object at path '${optionsArg.path}' in bucket '${this.name}':`,
747
- error
748
- );
749
745
  throw error;
750
746
  } finally {
751
747
  operation.finish();
@@ -809,9 +805,6 @@ export class Bucket {
809
805
  await operation.runProvider(
810
806
  () => operation.client.send(command, { abortSignal: operation.signal }),
811
807
  );
812
- } catch (err) {
813
- console.error('Error updating metadata:', err);
814
- throw err; // rethrow to allow caller to handle
815
808
  } finally {
816
809
  operation.finish();
817
810
  }
@@ -826,40 +819,20 @@ export class Bucket {
826
819
  targetBucket?: Bucket;
827
820
  overwrite?: boolean;
828
821
  }): Promise<void> {
829
- try {
830
- const destinationBucket = optionsArg.targetBucket || this;
831
- const exists = await destinationBucket.fastExists({
832
- path: optionsArg.destinationPath,
833
- });
834
-
835
- if (exists && !optionsArg.overwrite) {
836
- console.error(
837
- `Object already exists at destination path '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`
838
- );
839
- return;
840
- } else if (exists && optionsArg.overwrite) {
841
- console.log(
842
- `Overwriting existing object at destination path '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`
843
- );
844
- } else {
845
- console.log(
846
- `Moving object to path '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`
847
- );
848
- }
849
-
850
- await this.fastCopy(optionsArg);
851
- await this.fastRemove({ path: optionsArg.sourcePath });
822
+ const destinationBucket = optionsArg.targetBucket || this;
823
+ const exists = await destinationBucket.fastExists({
824
+ path: optionsArg.destinationPath,
825
+ });
852
826
 
853
- console.log(
854
- `Object '${optionsArg.sourcePath}' has been successfully moved to '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`
855
- );
856
- } catch (error) {
827
+ if (exists && !optionsArg.overwrite) {
857
828
  console.error(
858
- `Error moving object from '${optionsArg.sourcePath}' to '${optionsArg.destinationPath}':`,
859
- error
829
+ `Object already exists at destination path '${optionsArg.destinationPath}' in bucket '${destinationBucket.name}'.`
860
830
  );
861
- throw error;
831
+ return;
862
832
  }
833
+
834
+ await this.fastCopy(optionsArg);
835
+ await this.fastRemove({ path: optionsArg.sourcePath });
863
836
  }
864
837
 
865
838
  /**
@@ -895,7 +868,6 @@ export class Bucket {
895
868
  await operation.runProvider(
896
869
  () => operation.client.send(command, { abortSignal: operation.signal }),
897
870
  );
898
- console.log(`Object '${optionsArg.path}' exists in bucket '${this.name}'.`);
899
871
  return true;
900
872
  } catch (error: any) {
901
873
  try {
@@ -904,10 +876,8 @@ export class Bucket {
904
876
  throw abortError;
905
877
  }
906
878
  if (error?.name === 'NotFound') {
907
- console.log(`Object '${optionsArg.path}' does not exist in bucket '${this.name}'.`);
908
879
  return false;
909
880
  } else {
910
- console.error('Error checking object existence:', error);
911
881
  throw error; // Rethrow if it's not a NotFound error to handle unexpected issues
912
882
  }
913
883
  } finally {
@@ -967,27 +937,19 @@ export class Bucket {
967
937
  }
968
938
 
969
939
  public async getMagicBytes(optionsArg: { path: string; length: number }): Promise<Buffer> {
970
- try {
971
- const command = new plugins.s3.GetObjectCommand({
972
- Bucket: this.name,
973
- Key: optionsArg.path,
974
- Range: `bytes=0-${optionsArg.length - 1}`,
975
- });
976
- const response = await this.smartbucketRef.storageClient.send(command);
977
- const chunks: Buffer[] = [];
978
- const stream = response.Body as any; // SdkStreamMixin includes readable stream
940
+ const command = new plugins.s3.GetObjectCommand({
941
+ Bucket: this.name,
942
+ Key: optionsArg.path,
943
+ Range: `bytes=0-${optionsArg.length - 1}`,
944
+ });
945
+ const response = await this.smartbucketRef.storageClient.send(command);
946
+ const chunks: Buffer[] = [];
947
+ const stream = response.Body as any; // SdkStreamMixin includes readable stream
979
948
 
980
- for await (const chunk of stream) {
981
- chunks.push(chunk);
982
- }
983
- return Buffer.concat(chunks);
984
- } catch (error) {
985
- console.error(
986
- `Error retrieving magic bytes from object at path '${optionsArg.path}' in bucket '${this.name}':`,
987
- error
988
- );
989
- throw error;
949
+ for await (const chunk of stream) {
950
+ chunks.push(chunk);
990
951
  }
952
+ return Buffer.concat(chunks);
991
953
  }
992
954
 
993
955
  // ==========================================
@@ -1240,50 +1202,41 @@ export class Bucket {
1240
1202
  }
1241
1203
 
1242
1204
  public async cleanAllContents(): Promise<void> {
1243
- try {
1244
- // Define the command type explicitly
1245
- const listCommandInput: plugins.s3.ListObjectsV2CommandInput = {
1246
- Bucket: this.name,
1247
- };
1248
-
1249
- let isTruncated = true;
1250
- let continuationToken: string | undefined = undefined;
1251
-
1252
- while (isTruncated) {
1253
- // Add the continuation token to the input if present
1254
- const listCommand = new plugins.s3.ListObjectsV2Command({
1255
- ...listCommandInput,
1256
- ContinuationToken: continuationToken,
1205
+ // Define the command type explicitly
1206
+ const listCommandInput: plugins.s3.ListObjectsV2CommandInput = {
1207
+ Bucket: this.name,
1208
+ };
1209
+
1210
+ let isTruncated = true;
1211
+ let continuationToken: string | undefined = undefined;
1212
+
1213
+ while (isTruncated) {
1214
+ // Add the continuation token to the input if present
1215
+ const listCommand = new plugins.s3.ListObjectsV2Command({
1216
+ ...listCommandInput,
1217
+ ContinuationToken: continuationToken,
1218
+ });
1219
+
1220
+ // Explicitly type the response
1221
+ const response: plugins.s3.ListObjectsV2Output =
1222
+ await this.smartbucketRef.storageClient.send(listCommand);
1223
+
1224
+ if (response.Contents && response.Contents.length > 0) {
1225
+ // Delete objects in batches, mapping each item to { Key: string }
1226
+ const deleteCommand = new plugins.s3.DeleteObjectsCommand({
1227
+ Bucket: this.name,
1228
+ Delete: {
1229
+ Objects: response.Contents.map((item) => ({ Key: item.Key! })),
1230
+ Quiet: true,
1231
+ },
1257
1232
  });
1258
-
1259
- // Explicitly type the response
1260
- const response: plugins.s3.ListObjectsV2Output =
1261
- await this.smartbucketRef.storageClient.send(listCommand);
1262
-
1263
- console.log(`Cleaning contents of bucket '${this.name}': Now deleting ${response.Contents?.length} items...`);
1264
-
1265
- if (response.Contents && response.Contents.length > 0) {
1266
- // Delete objects in batches, mapping each item to { Key: string }
1267
- const deleteCommand = new plugins.s3.DeleteObjectsCommand({
1268
- Bucket: this.name,
1269
- Delete: {
1270
- Objects: response.Contents.map((item) => ({ Key: item.Key! })),
1271
- Quiet: true,
1272
- },
1273
- });
1274
-
1275
- await this.smartbucketRef.storageClient.send(deleteCommand);
1276
- }
1277
-
1278
- // Update continuation token and truncation status
1279
- isTruncated = response.IsTruncated || false;
1280
- continuationToken = response.NextContinuationToken;
1233
+
1234
+ await this.smartbucketRef.storageClient.send(deleteCommand);
1281
1235
  }
1282
-
1283
- console.log(`All contents in bucket '${this.name}' have been deleted.`);
1284
- } catch (error) {
1285
- console.error(`Error cleaning contents of bucket '${this.name}':`, error);
1286
- throw error;
1236
+
1237
+ // Update continuation token and truncation status
1238
+ isTruncated = response.IsTruncated || false;
1239
+ continuationToken = response.NextContinuationToken;
1287
1240
  }
1288
1241
  }
1289
1242
  }
@@ -179,30 +179,25 @@ export class Directory {
179
179
  * lists all folders
180
180
  */
181
181
  public async listDirectories(): Promise<Directory[]> {
182
- try {
183
- const { commonPrefixes } = await this.listObjectsV2AllPages(this.getBasePath(), '/');
184
- const directoryArray: Directory[] = [];
185
-
186
- if (commonPrefixes) {
187
- commonPrefixes.forEach((item) => {
188
- if (item.Prefix) {
189
- const subtractedPath = item.Prefix.replace(this.getBasePath(), '');
190
- if (subtractedPath.endsWith('/')) {
191
- const dirName = subtractedPath.slice(0, -1);
192
- // Ensure the directory name is not empty (which would indicate the base directory itself)
193
- if (dirName) {
194
- directoryArray.push(new Directory(this.bucketRef, this, dirName));
195
- }
182
+ const { commonPrefixes } = await this.listObjectsV2AllPages(this.getBasePath(), '/');
183
+ const directoryArray: Directory[] = [];
184
+
185
+ if (commonPrefixes) {
186
+ commonPrefixes.forEach((item) => {
187
+ if (item.Prefix) {
188
+ const subtractedPath = item.Prefix.replace(this.getBasePath(), '');
189
+ if (subtractedPath.endsWith('/')) {
190
+ const dirName = subtractedPath.slice(0, -1);
191
+ // Ensure the directory name is not empty (which would indicate the base directory itself)
192
+ if (dirName) {
193
+ directoryArray.push(new Directory(this.bucketRef, this, dirName));
196
194
  }
197
195
  }
198
- });
199
- }
200
-
201
- return directoryArray;
202
- } catch (error) {
203
- console.error('Error listing directories:', error);
204
- throw error;
196
+ }
197
+ });
205
198
  }
199
+
200
+ return directoryArray;
206
201
  }
207
202
 
208
203
  /**
@@ -409,12 +404,8 @@ export class Directory {
409
404
  }) {
410
405
  const deleteDirectory = async (directoryArg: Directory) => {
411
406
  const childDirectories = await directoryArg.listDirectories();
412
- if (childDirectories.length === 0) {
413
- console.log('Directory empty! Path complete!');
414
- } else {
415
- for (const childDir of childDirectories) {
416
- await deleteDirectory(childDir);
417
- }
407
+ for (const childDir of childDirectories) {
408
+ await deleteDirectory(childDir);
418
409
  }
419
410
  const files = await directoryArg.listFiles();
420
411
  for (const file of files) {
@@ -3,6 +3,7 @@
3
3
  import * as plugins from './plugins.js';
4
4
  import { Bucket } from './classes.bucket.js';
5
5
  import { normalizeStorageDescriptor } from './helpers.js';
6
+ import { headBucketExists } from './internal.bucketrequests.js';
6
7
  import {
7
8
  beginOperationLifecycleClose,
8
9
  commitOperationLifecycleClient,
@@ -12,6 +13,7 @@ import {
12
13
  rollbackOperationLifecycleClientMutation,
13
14
  } from './internal.exactupload.capability.js';
14
15
  import type {
16
+ IBucketRequestOptions,
15
17
  ISmartBucketReadinessOptions,
16
18
  TSmartBucketReadinessFailureReason,
17
19
  TSmartBucketReadinessResult,
@@ -107,26 +109,34 @@ export class SmartBucket {
107
109
  registerOperationLifecycle(this, this.currentStorageClient);
108
110
  }
109
111
 
110
- public async createBucket(bucketNameArg: string) {
111
- const bucket = await Bucket.createBucketByName(this, bucketNameArg);
112
+ public async createBucket(bucketNameArg: string, optionsArg: IBucketRequestOptions = {}) {
113
+ const bucket = await Bucket.createBucketByName(this, bucketNameArg, optionsArg);
112
114
  return bucket;
113
115
  }
114
116
 
115
- public async removeBucket(bucketName: string) {
116
- await Bucket.removeBucketByName(this, bucketName);
117
+ public async removeBucket(bucketName: string, optionsArg: IBucketRequestOptions = {}) {
118
+ await Bucket.removeBucketByName(this, bucketName, optionsArg);
117
119
  }
118
120
 
119
- public async getBucketByName(bucketNameArg: string) {
120
- return Bucket.getBucketByName(this, bucketNameArg);
121
+ /**
122
+ * Returns a reference to an existing bucket, checked with one HeadBucket
123
+ * request. Rejects when the bucket does not exist or the credentials may
124
+ * not access it.
125
+ */
126
+ public async getBucketByName(bucketNameArg: string, optionsArg: IBucketRequestOptions = {}) {
127
+ return Bucket.getBucketByName(this, bucketNameArg, optionsArg);
121
128
  }
122
129
 
123
130
  /**
124
- * Check if a bucket exists
131
+ * Checks with one HeadBucket request whether a bucket exists. Resolves
132
+ * `false` for a missing bucket and rejects when the credentials may not
133
+ * access it.
125
134
  */
126
- public async bucketExists(bucketNameArg: string): Promise<boolean> {
127
- const command = new plugins.s3.ListBucketsCommand({});
128
- const buckets = await this.currentStorageClient.send(command);
129
- return buckets.Buckets?.some(bucket => bucket.Name === bucketNameArg) ?? false;
135
+ public async bucketExists(
136
+ bucketNameArg: string,
137
+ optionsArg: IBucketRequestOptions = {},
138
+ ): Promise<boolean> {
139
+ return headBucketExists(this, bucketNameArg, optionsArg.signal);
130
140
  }
131
141
 
132
142
  /**
@@ -83,7 +83,6 @@ export class BucketWatcher extends EventEmitter {
83
83
  */
84
84
  public async start(): Promise<void> {
85
85
  if (this.pollIntervalId !== null) {
86
- console.log('BucketWatcher is already running');
87
86
  return;
88
87
  }
89
88
 
@@ -52,6 +52,15 @@ export interface IDirectoryFastPutStreamOptions {
52
52
  signal?: AbortSignal;
53
53
  }
54
54
 
55
+ /**
56
+ * Options for a request about a bucket itself: `getBucketByName()`,
57
+ * `bucketExists()`, `createBucket()` and `removeBucket()`.
58
+ */
59
+ export interface IBucketRequestOptions {
60
+ /** Optional caller-owned cancellation signal */
61
+ signal?: AbortSignal;
62
+ }
63
+
55
64
  // ================================
56
65
  // SmartBucket Readiness Interfaces
57
66
  // ================================
@@ -0,0 +1,70 @@
1
+ import * as plugins from './plugins.js';
2
+ import type { SmartBucket } from './classes.smartbucket.js';
3
+ import { beginAccountOperation } from './internal.exactupload.capability.js';
4
+
5
+ /**
6
+ * Sends one account-scoped request under the SmartBucket lifecycle. The
7
+ * caller's signal and `SmartBucket.close()` abort the in-flight SDK request.
8
+ */
9
+ export const runAccountRequest = async <T>(
10
+ smartbucketRefArg: SmartBucket,
11
+ signalArg: AbortSignal | undefined,
12
+ sendArg: (clientArg: plugins.s3.S3Client, abortSignalArg: AbortSignal) => Promise<T>,
13
+ ): Promise<T> => {
14
+ const operation = beginAccountOperation(smartbucketRefArg, signalArg);
15
+ try {
16
+ return await operation.runProvider(() => sendArg(operation.client, operation.signal));
17
+ } finally {
18
+ operation.finish();
19
+ }
20
+ };
21
+
22
+ const getProviderStatus = (errorArg: unknown): { name?: string; httpStatusCode?: number } => {
23
+ if (typeof errorArg !== 'object' || errorArg === null) {
24
+ return {};
25
+ }
26
+ const providerError = errorArg as {
27
+ name?: unknown;
28
+ $metadata?: { httpStatusCode?: unknown };
29
+ };
30
+ return {
31
+ name: typeof providerError.name === 'string' ? providerError.name : undefined,
32
+ httpStatusCode: typeof providerError.$metadata?.httpStatusCode === 'number'
33
+ ? providerError.$metadata.httpStatusCode
34
+ : undefined,
35
+ };
36
+ };
37
+
38
+ /**
39
+ * Answers whether the bucket exists with one HeadBucket request, which a
40
+ * bucket-scoped credential may send for its own bucket. A missing bucket
41
+ * resolves `false`; a bucket the credentials may not access rejects, because
42
+ * S3 does not say whether it exists.
43
+ */
44
+ export const headBucketExists = async (
45
+ smartbucketRefArg: SmartBucket,
46
+ bucketNameArg: string,
47
+ signalArg: AbortSignal | undefined,
48
+ ): Promise<boolean> => {
49
+ try {
50
+ await runAccountRequest(smartbucketRefArg, signalArg, (clientArg, abortSignalArg) =>
51
+ clientArg.send(new plugins.s3.HeadBucketCommand({ Bucket: bucketNameArg }), {
52
+ abortSignal: abortSignalArg,
53
+ }),
54
+ );
55
+ return true;
56
+ } catch (error) {
57
+ const { name, httpStatusCode } = getProviderStatus(error);
58
+ if (httpStatusCode === 404 || name === 'NotFound' || name === 'NoSuchBucket') {
59
+ return false;
60
+ }
61
+ if (httpStatusCode === 403 || name === 'Forbidden' || name === 'AccessDenied') {
62
+ throw new Error(
63
+ `Access to bucket '${bucketNameArg}' was denied (HTTP 403): the credentials may not ` +
64
+ `read this bucket, or it belongs to another account.`,
65
+ { cause: error },
66
+ );
67
+ }
68
+ throw error;
69
+ }
70
+ };