s3db.js 11.2.3 → 11.2.4

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 (42) hide show
  1. package/dist/s3db.cjs.js +1177 -128
  2. package/dist/s3db.cjs.js.map +1 -1
  3. package/dist/s3db.es.js +1172 -129
  4. package/dist/s3db.es.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/behaviors/enforce-limits.js +28 -4
  7. package/src/behaviors/index.js +6 -1
  8. package/src/client.class.js +11 -1
  9. package/src/concerns/partition-queue.js +7 -1
  10. package/src/concerns/plugin-storage.js +75 -13
  11. package/src/database.class.js +19 -4
  12. package/src/errors.js +306 -27
  13. package/src/partition-drivers/base-partition-driver.js +12 -2
  14. package/src/partition-drivers/index.js +7 -1
  15. package/src/partition-drivers/memory-partition-driver.js +20 -5
  16. package/src/partition-drivers/sqs-partition-driver.js +6 -1
  17. package/src/plugins/audit.errors.js +46 -0
  18. package/src/plugins/backup/base-backup-driver.class.js +36 -6
  19. package/src/plugins/backup/filesystem-backup-driver.class.js +55 -7
  20. package/src/plugins/backup/index.js +40 -9
  21. package/src/plugins/backup/multi-backup-driver.class.js +69 -9
  22. package/src/plugins/backup/s3-backup-driver.class.js +48 -6
  23. package/src/plugins/backup.errors.js +45 -0
  24. package/src/plugins/cache/cache.class.js +8 -1
  25. package/src/plugins/cache.errors.js +47 -0
  26. package/src/plugins/cache.plugin.js +8 -1
  27. package/src/plugins/fulltext.errors.js +46 -0
  28. package/src/plugins/fulltext.plugin.js +15 -3
  29. package/src/plugins/metrics.errors.js +46 -0
  30. package/src/plugins/queue-consumer.plugin.js +31 -4
  31. package/src/plugins/queue.errors.js +46 -0
  32. package/src/plugins/replicator.errors.js +46 -0
  33. package/src/plugins/replicator.plugin.js +40 -5
  34. package/src/plugins/replicators/base-replicator.class.js +19 -3
  35. package/src/plugins/replicators/index.js +9 -3
  36. package/src/plugins/replicators/s3db-replicator.class.js +38 -8
  37. package/src/plugins/scheduler.errors.js +46 -0
  38. package/src/plugins/scheduler.plugin.js +79 -19
  39. package/src/plugins/state-machine.errors.js +47 -0
  40. package/src/plugins/state-machine.plugin.js +86 -17
  41. package/src/stream/index.js +6 -1
  42. package/src/stream/resource-reader.class.js +6 -1
package/dist/s3db.cjs.js CHANGED
@@ -222,7 +222,7 @@ function calculateEffectiveLimit(config = {}) {
222
222
  }
223
223
 
224
224
  class BaseError extends Error {
225
- constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata, suggestion, description, ...rest }) {
225
+ constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata, description, ...rest }) {
226
226
  if (verbose) message = message + `
227
227
 
228
228
  Verbose:
@@ -247,7 +247,6 @@ ${JSON.stringify(rest, null, 2)}`;
247
247
  this.commandName = commandName;
248
248
  this.commandInput = commandInput;
249
249
  this.metadata = metadata;
250
- this.suggestion = suggestion;
251
250
  this.description = description;
252
251
  this.data = { bucket, key, ...rest, verbose, message };
253
252
  }
@@ -265,7 +264,6 @@ ${JSON.stringify(rest, null, 2)}`;
265
264
  commandName: this.commandName,
266
265
  commandInput: this.commandInput,
267
266
  metadata: this.metadata,
268
- suggestion: this.suggestion,
269
267
  description: this.description,
270
268
  data: this.data,
271
269
  original: this.original,
@@ -406,26 +404,26 @@ function mapAwsError(err, context = {}) {
406
404
  const metadata = err.$metadata ? { ...err.$metadata } : void 0;
407
405
  const commandName = context.commandName;
408
406
  const commandInput = context.commandInput;
409
- let suggestion;
407
+ let description;
410
408
  if (code === "NoSuchKey" || code === "NotFound") {
411
- suggestion = "Check if the key exists in the specified bucket and if your credentials have permission.";
412
- return new NoSuchKey({ ...context, original: err, metadata, commandName, commandInput, suggestion });
409
+ description = "The specified key does not exist in the bucket. Check if the key exists and if your credentials have permission to access it.";
410
+ return new NoSuchKey({ ...context, original: err, metadata, commandName, commandInput, description });
413
411
  }
414
412
  if (code === "NoSuchBucket") {
415
- suggestion = "Check if the bucket exists and if your credentials have permission.";
416
- return new NoSuchBucket({ ...context, original: err, metadata, commandName, commandInput, suggestion });
413
+ description = "The specified bucket does not exist. Check if the bucket name is correct and if your credentials have permission to access it.";
414
+ return new NoSuchBucket({ ...context, original: err, metadata, commandName, commandInput, description });
417
415
  }
418
416
  if (code === "AccessDenied" || err.statusCode === 403 || code === "Forbidden") {
419
- suggestion = "Check your credentials and bucket policy.";
420
- return new PermissionError("Access denied", { ...context, original: err, metadata, commandName, commandInput, suggestion });
417
+ description = "Access denied. Check your AWS credentials, IAM permissions, and bucket policy.";
418
+ return new PermissionError("Access denied", { ...context, original: err, metadata, commandName, commandInput, description });
421
419
  }
422
420
  if (code === "ValidationError" || err.statusCode === 400) {
423
- suggestion = "Check the request parameters and payload.";
424
- return new ValidationError("Validation error", { ...context, original: err, metadata, commandName, commandInput, suggestion });
421
+ description = "Validation error. Check the request parameters and payload format.";
422
+ return new ValidationError("Validation error", { ...context, original: err, metadata, commandName, commandInput, description });
425
423
  }
426
424
  if (code === "MissingMetadata") {
427
- suggestion = "Check if the object metadata is present and valid.";
428
- return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput, suggestion });
425
+ description = "Object metadata is missing or invalid. Check if the object was uploaded correctly.";
426
+ return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput, description });
429
427
  }
430
428
  const errorDetails = [
431
429
  `Unknown error: ${err.message || err.toString()}`,
@@ -433,27 +431,31 @@ function mapAwsError(err, context = {}) {
433
431
  err.statusCode && `Status: ${err.statusCode}`,
434
432
  err.stack && `Stack: ${err.stack.split("\n")[0]}`
435
433
  ].filter(Boolean).join(" | ");
436
- suggestion = `Check the error details and AWS documentation. Original error: ${err.message || err.toString()}`;
437
- return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput, suggestion });
434
+ description = `Check the error details and AWS documentation. Original error: ${err.message || err.toString()}`;
435
+ return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput, description });
438
436
  }
439
437
  class ConnectionStringError extends S3dbError {
440
438
  constructor(message, details = {}) {
441
- super(message, { ...details, suggestion: "Check the connection string format and credentials." });
439
+ const description = details.description || "Invalid connection string format. Check the connection string syntax and credentials.";
440
+ super(message, { ...details, description });
442
441
  }
443
442
  }
444
443
  class CryptoError extends S3dbError {
445
444
  constructor(message, details = {}) {
446
- super(message, { ...details, suggestion: "Check if the crypto library is available and input is valid." });
445
+ const description = details.description || "Cryptography operation failed. Check if the crypto library is available and input is valid.";
446
+ super(message, { ...details, description });
447
447
  }
448
448
  }
449
449
  class SchemaError extends S3dbError {
450
450
  constructor(message, details = {}) {
451
- super(message, { ...details, suggestion: "Check schema definition and input data." });
451
+ const description = details.description || "Schema validation failed. Check schema definition and input data format.";
452
+ super(message, { ...details, description });
452
453
  }
453
454
  }
454
455
  class ResourceError extends S3dbError {
455
456
  constructor(message, details = {}) {
456
- super(message, { ...details, suggestion: details.suggestion || "Check resource configuration, attributes, and operation context." });
457
+ const description = details.description || "Resource operation failed. Check resource configuration, attributes, and operation context.";
458
+ super(message, { ...details, description });
457
459
  Object.assign(this, details);
458
460
  }
459
461
  }
@@ -482,13 +484,12 @@ ${details.strictValidation === false ? " \u2022 Update partition definition to
482
484
  \u2022 Update partition definition to use existing fields, OR
483
485
  \u2022 Use strictValidation: false to skip this check during testing`}
484
486
 
485
- Docs: https://docs.s3db.js.org/resources/partitions#validation
487
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partitions
486
488
  `.trim();
487
489
  }
488
490
  super(message, {
489
491
  ...details,
490
- description,
491
- suggestion: details.suggestion || "Check partition definition, fields, and input values."
492
+ description
492
493
  });
493
494
  }
494
495
  }
@@ -547,7 +548,7 @@ Example fix:
547
548
  await db.connect(); // Plugin initialized here
548
549
  await db.createResource({ name: '${resourceName}', ... }); // Analytics resource created here
549
550
 
550
- Docs: https://docs.s3db.js.org/plugins/eventual-consistency#troubleshooting
551
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/eventual-consistency.md
551
552
  `.trim();
552
553
  super(message, {
553
554
  ...rest,
@@ -557,8 +558,260 @@ Docs: https://docs.s3db.js.org/plugins/eventual-consistency#troubleshooting
557
558
  configuredResources,
558
559
  registeredResources,
559
560
  pluginInitialized,
560
- description,
561
- suggestion: "Ensure resources are created after plugin initialization. Check plugin configuration and resource creation order."
561
+ description
562
+ });
563
+ }
564
+ }
565
+ class PluginError extends S3dbError {
566
+ constructor(message, details = {}) {
567
+ const {
568
+ pluginName = "Unknown",
569
+ operation = "unknown",
570
+ ...rest
571
+ } = details;
572
+ let description = details.description;
573
+ if (!description) {
574
+ description = `
575
+ Plugin Error
576
+
577
+ Plugin: ${pluginName}
578
+ Operation: ${operation}
579
+
580
+ Possible causes:
581
+ 1. Plugin not properly initialized
582
+ 2. Plugin configuration is invalid
583
+ 3. Plugin dependencies not met
584
+ 4. Plugin method called before installation
585
+
586
+ Solution:
587
+ Ensure plugin is added to database and connect() is called before usage.
588
+
589
+ Example:
590
+ const db = new Database({
591
+ bucket: 'my-bucket',
592
+ plugins: [new ${pluginName}({ /* config */ })]
593
+ });
594
+
595
+ await db.connect(); // Plugin installed here
596
+ // Now plugin methods are available
597
+
598
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/README.md
599
+ `.trim();
600
+ }
601
+ super(message, {
602
+ ...rest,
603
+ pluginName,
604
+ operation,
605
+ description
606
+ });
607
+ }
608
+ }
609
+ class PluginStorageError extends S3dbError {
610
+ constructor(message, details = {}) {
611
+ const {
612
+ pluginSlug = "unknown",
613
+ key = "",
614
+ operation = "unknown",
615
+ ...rest
616
+ } = details;
617
+ let description = details.description;
618
+ if (!description) {
619
+ description = `
620
+ Plugin Storage Error
621
+
622
+ Plugin: ${pluginSlug}
623
+ Key: ${key}
624
+ Operation: ${operation}
625
+
626
+ Possible causes:
627
+ 1. Storage not initialized (plugin not installed)
628
+ 2. Invalid key format
629
+ 3. S3 operation failed
630
+ 4. Permissions issue
631
+
632
+ Solution:
633
+ Ensure plugin has access to storage and key is valid.
634
+
635
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/README.md#plugin-storage
636
+ `.trim();
637
+ }
638
+ super(message, {
639
+ ...rest,
640
+ pluginSlug,
641
+ key,
642
+ operation,
643
+ description
644
+ });
645
+ }
646
+ }
647
+ class PartitionDriverError extends S3dbError {
648
+ constructor(message, details = {}) {
649
+ const {
650
+ driver = "unknown",
651
+ operation = "unknown",
652
+ queueSize,
653
+ maxQueueSize,
654
+ ...rest
655
+ } = details;
656
+ let description = details.description;
657
+ if (!description && queueSize !== void 0 && maxQueueSize !== void 0) {
658
+ description = `
659
+ Partition Driver Error
660
+
661
+ Driver: ${driver}
662
+ Operation: ${operation}
663
+ Queue Status: ${queueSize}/${maxQueueSize}
664
+
665
+ Possible causes:
666
+ 1. Queue is full (backpressure)
667
+ 2. Driver not properly configured
668
+ 3. SQS permissions issue (if using SQS driver)
669
+
670
+ Solution:
671
+ ${queueSize >= maxQueueSize ? "Wait for queue to drain or increase maxQueueSize" : "Check driver configuration and permissions"}
672
+
673
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partition-drivers
674
+ `.trim();
675
+ } else if (!description) {
676
+ description = `
677
+ Partition Driver Error
678
+
679
+ Driver: ${driver}
680
+ Operation: ${operation}
681
+
682
+ Check driver configuration and permissions.
683
+
684
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partition-drivers
685
+ `.trim();
686
+ }
687
+ super(message, {
688
+ ...rest,
689
+ driver,
690
+ operation,
691
+ queueSize,
692
+ maxQueueSize,
693
+ description
694
+ });
695
+ }
696
+ }
697
+ class BehaviorError extends S3dbError {
698
+ constructor(message, details = {}) {
699
+ const {
700
+ behavior = "unknown",
701
+ availableBehaviors = [],
702
+ ...rest
703
+ } = details;
704
+ let description = details.description;
705
+ if (!description) {
706
+ description = `
707
+ Behavior Error
708
+
709
+ Requested: ${behavior}
710
+ Available: ${availableBehaviors.join(", ") || "body-overflow, body-only, truncate-data, enforce-limits, user-managed"}
711
+
712
+ Possible causes:
713
+ 1. Behavior name misspelled
714
+ 2. Custom behavior not registered
715
+
716
+ Solution:
717
+ Use one of the available behaviors or register custom behavior.
718
+
719
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#behaviors
720
+ `.trim();
721
+ }
722
+ super(message, {
723
+ ...rest,
724
+ behavior,
725
+ availableBehaviors,
726
+ description
727
+ });
728
+ }
729
+ }
730
+ class StreamError extends S3dbError {
731
+ constructor(message, details = {}) {
732
+ const {
733
+ operation = "unknown",
734
+ resource,
735
+ ...rest
736
+ } = details;
737
+ let description = details.description;
738
+ if (!description) {
739
+ description = `
740
+ Stream Error
741
+
742
+ Operation: ${operation}
743
+ ${resource ? `Resource: ${resource}` : ""}
744
+
745
+ Possible causes:
746
+ 1. Stream not properly initialized
747
+ 2. Resource not available
748
+ 3. Network error during streaming
749
+
750
+ Solution:
751
+ Check stream configuration and resource availability.
752
+
753
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#streaming
754
+ `.trim();
755
+ }
756
+ super(message, {
757
+ ...rest,
758
+ operation,
759
+ resource,
760
+ description
761
+ });
762
+ }
763
+ }
764
+ class MetadataLimitError extends S3dbError {
765
+ constructor(message, details = {}) {
766
+ const {
767
+ totalSize,
768
+ effectiveLimit,
769
+ absoluteLimit = 2047,
770
+ excess,
771
+ resourceName,
772
+ operation,
773
+ ...rest
774
+ } = details;
775
+ let description = details.description;
776
+ if (!description && totalSize && effectiveLimit) {
777
+ description = `
778
+ S3 Metadata Size Limit Exceeded
779
+
780
+ Current Size: ${totalSize} bytes
781
+ Effective Limit: ${effectiveLimit} bytes
782
+ Absolute Limit: ${absoluteLimit} bytes
783
+ ${excess ? `Excess: ${excess} bytes` : ""}
784
+ ${resourceName ? `Resource: ${resourceName}` : ""}
785
+ ${operation ? `Operation: ${operation}` : ""}
786
+
787
+ S3 has a hard limit of 2KB (2047 bytes) for object metadata.
788
+
789
+ Solutions:
790
+ 1. Use 'body-overflow' behavior to store excess in body
791
+ 2. Use 'body-only' behavior to store everything in body
792
+ 3. Reduce number of fields
793
+ 4. Use shorter field values
794
+ 5. Enable advanced metadata encoding
795
+
796
+ Example:
797
+ await db.createResource({
798
+ name: '${resourceName || "myResource"}',
799
+ behavior: 'body-overflow', // Automatically handles overflow
800
+ attributes: { ... }
801
+ });
802
+
803
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#metadata-size-limits
804
+ `.trim();
805
+ }
806
+ super(message, {
807
+ ...rest,
808
+ totalSize,
809
+ effectiveLimit,
810
+ absoluteLimit,
811
+ excess,
812
+ resourceName,
813
+ operation,
814
+ description
562
815
  });
563
816
  }
564
817
  }
@@ -902,10 +1155,17 @@ class PluginStorage {
902
1155
  */
903
1156
  constructor(client, pluginSlug) {
904
1157
  if (!client) {
905
- throw new Error("PluginStorage requires a client instance");
1158
+ throw new PluginStorageError("PluginStorage requires a client instance", {
1159
+ operation: "constructor",
1160
+ pluginSlug,
1161
+ suggestion: "Pass a valid S3db Client instance when creating PluginStorage"
1162
+ });
906
1163
  }
907
1164
  if (!pluginSlug) {
908
- throw new Error("PluginStorage requires a pluginSlug");
1165
+ throw new PluginStorageError("PluginStorage requires a pluginSlug", {
1166
+ operation: "constructor",
1167
+ suggestion: 'Provide a plugin slug (e.g., "eventual-consistency", "cache", "audit")'
1168
+ });
909
1169
  }
910
1170
  this.client = client;
911
1171
  this.pluginSlug = pluginSlug;
@@ -958,7 +1218,15 @@ class PluginStorage {
958
1218
  }
959
1219
  const [ok, err] = await tryFn(() => this.client.putObject(putParams));
960
1220
  if (!ok) {
961
- throw new Error(`PluginStorage.set failed for key ${key}: ${err.message}`);
1221
+ throw new PluginStorageError(`Failed to save plugin data`, {
1222
+ pluginSlug: this.pluginSlug,
1223
+ key,
1224
+ operation: "set",
1225
+ behavior,
1226
+ ttl,
1227
+ original: err,
1228
+ suggestion: "Check S3 permissions and key format"
1229
+ });
962
1230
  }
963
1231
  }
964
1232
  /**
@@ -980,7 +1248,13 @@ class PluginStorage {
980
1248
  if (err.name === "NoSuchKey" || err.Code === "NoSuchKey") {
981
1249
  return null;
982
1250
  }
983
- throw new Error(`PluginStorage.get failed for key ${key}: ${err.message}`);
1251
+ throw new PluginStorageError(`Failed to retrieve plugin data`, {
1252
+ pluginSlug: this.pluginSlug,
1253
+ key,
1254
+ operation: "get",
1255
+ original: err,
1256
+ suggestion: "Check if the key exists and S3 permissions are correct"
1257
+ });
984
1258
  }
985
1259
  const metadata = response.Metadata || {};
986
1260
  const parsedMetadata = this._parseMetadataValues(metadata);
@@ -993,7 +1267,13 @@ class PluginStorage {
993
1267
  data = { ...parsedMetadata, ...body };
994
1268
  }
995
1269
  } catch (parseErr) {
996
- throw new Error(`PluginStorage.get failed to parse body for key ${key}: ${parseErr.message}`);
1270
+ throw new PluginStorageError(`Failed to parse JSON body`, {
1271
+ pluginSlug: this.pluginSlug,
1272
+ key,
1273
+ operation: "get",
1274
+ original: parseErr,
1275
+ suggestion: "Body content may be corrupted. Check S3 object integrity"
1276
+ });
997
1277
  }
998
1278
  }
999
1279
  const expiresAt = data._expiresat || data._expiresAt;
@@ -1054,7 +1334,15 @@ class PluginStorage {
1054
1334
  () => this.client.listObjects({ prefix: fullPrefix, maxKeys: limit })
1055
1335
  );
1056
1336
  if (!ok) {
1057
- throw new Error(`PluginStorage.list failed: ${err.message}`);
1337
+ throw new PluginStorageError(`Failed to list plugin data`, {
1338
+ pluginSlug: this.pluginSlug,
1339
+ operation: "list",
1340
+ prefix,
1341
+ fullPrefix,
1342
+ limit,
1343
+ original: err,
1344
+ suggestion: "Check S3 permissions and bucket configuration"
1345
+ });
1058
1346
  }
1059
1347
  const keys = result.Contents?.map((item) => item.Key) || [];
1060
1348
  return this._removeKeyPrefix(keys);
@@ -1074,7 +1362,16 @@ class PluginStorage {
1074
1362
  () => this.client.listObjects({ prefix: fullPrefix, maxKeys: limit })
1075
1363
  );
1076
1364
  if (!ok) {
1077
- throw new Error(`PluginStorage.listForResource failed: ${err.message}`);
1365
+ throw new PluginStorageError(`Failed to list resource data`, {
1366
+ pluginSlug: this.pluginSlug,
1367
+ operation: "listForResource",
1368
+ resourceName,
1369
+ subPrefix,
1370
+ fullPrefix,
1371
+ limit,
1372
+ original: err,
1373
+ suggestion: "Check resource name and S3 permissions"
1374
+ });
1078
1375
  }
1079
1376
  const keys = result.Contents?.map((item) => item.Key) || [];
1080
1377
  return this._removeKeyPrefix(keys);
@@ -1214,7 +1511,13 @@ class PluginStorage {
1214
1511
  async delete(key) {
1215
1512
  const [ok, err] = await tryFn(() => this.client.deleteObject(key));
1216
1513
  if (!ok) {
1217
- throw new Error(`PluginStorage.delete failed for key ${key}: ${err.message}`);
1514
+ throw new PluginStorageError(`Failed to delete plugin data`, {
1515
+ pluginSlug: this.pluginSlug,
1516
+ key,
1517
+ operation: "delete",
1518
+ original: err,
1519
+ suggestion: "Check S3 delete permissions"
1520
+ });
1218
1521
  }
1219
1522
  }
1220
1523
  /**
@@ -1401,16 +1704,28 @@ class PluginStorage {
1401
1704
  const valueSize = calculateUTF8Bytes(encoded);
1402
1705
  currentSize += keySize + valueSize;
1403
1706
  if (currentSize > effectiveLimit) {
1404
- throw new Error(
1405
- `Data exceeds metadata limit (${currentSize} > ${effectiveLimit} bytes). Use 'body-overflow' or 'body-only' behavior.`
1406
- );
1707
+ throw new MetadataLimitError(`Data exceeds metadata limit with enforce-limits behavior`, {
1708
+ totalSize: currentSize,
1709
+ effectiveLimit,
1710
+ absoluteLimit: S3_METADATA_LIMIT,
1711
+ excess: currentSize - effectiveLimit,
1712
+ operation: "PluginStorage.set",
1713
+ pluginSlug: this.pluginSlug,
1714
+ suggestion: "Use 'body-overflow' or 'body-only' behavior to handle large data"
1715
+ });
1407
1716
  }
1408
1717
  metadata[key] = jsonValue;
1409
1718
  }
1410
1719
  break;
1411
1720
  }
1412
1721
  default:
1413
- throw new Error(`Unknown behavior: ${behavior}. Use 'body-overflow', 'body-only', or 'enforce-limits'.`);
1722
+ throw new BehaviorError(`Unknown behavior: ${behavior}`, {
1723
+ behavior,
1724
+ availableBehaviors: ["body-overflow", "body-only", "enforce-limits"],
1725
+ operation: "PluginStorage._applyBehavior",
1726
+ pluginSlug: this.pluginSlug,
1727
+ suggestion: "Use 'body-overflow', 'body-only', or 'enforce-limits'"
1728
+ });
1414
1729
  }
1415
1730
  return { metadata, body };
1416
1731
  }
@@ -1975,6 +2290,35 @@ class AuditPlugin extends Plugin {
1975
2290
  }
1976
2291
  }
1977
2292
 
2293
+ class BackupError extends S3dbError {
2294
+ constructor(message, details = {}) {
2295
+ const { driver = "unknown", operation = "unknown", backupId, ...rest } = details;
2296
+ let description = details.description;
2297
+ if (!description) {
2298
+ description = `
2299
+ Backup Operation Error
2300
+
2301
+ Driver: ${driver}
2302
+ Operation: ${operation}
2303
+ ${backupId ? `Backup ID: ${backupId}` : ""}
2304
+
2305
+ Common causes:
2306
+ 1. Invalid backup driver configuration
2307
+ 2. Destination storage not accessible
2308
+ 3. Insufficient permissions
2309
+ 4. Network connectivity issues
2310
+ 5. Invalid backup file format
2311
+
2312
+ Solution:
2313
+ Check driver configuration and ensure destination storage is accessible.
2314
+
2315
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/backup.md
2316
+ `.trim();
2317
+ }
2318
+ super(message, { ...rest, driver, operation, backupId, description });
2319
+ }
2320
+ }
2321
+
1978
2322
  class BaseBackupDriver {
1979
2323
  constructor(config = {}) {
1980
2324
  this.config = {
@@ -2005,7 +2349,12 @@ class BaseBackupDriver {
2005
2349
  * @returns {Object} Upload result with destination info
2006
2350
  */
2007
2351
  async upload(filePath, backupId, manifest) {
2008
- throw new Error("upload() method must be implemented by subclass");
2352
+ throw new BackupError("upload() method must be implemented by subclass", {
2353
+ operation: "upload",
2354
+ driver: this.constructor.name,
2355
+ backupId,
2356
+ suggestion: "Extend BaseBackupDriver and implement the upload() method"
2357
+ });
2009
2358
  }
2010
2359
  /**
2011
2360
  * Download a backup file from the destination
@@ -2015,7 +2364,12 @@ class BaseBackupDriver {
2015
2364
  * @returns {string} Path to downloaded file
2016
2365
  */
2017
2366
  async download(backupId, targetPath, metadata) {
2018
- throw new Error("download() method must be implemented by subclass");
2367
+ throw new BackupError("download() method must be implemented by subclass", {
2368
+ operation: "download",
2369
+ driver: this.constructor.name,
2370
+ backupId,
2371
+ suggestion: "Extend BaseBackupDriver and implement the download() method"
2372
+ });
2019
2373
  }
2020
2374
  /**
2021
2375
  * Delete a backup from the destination
@@ -2023,7 +2377,12 @@ class BaseBackupDriver {
2023
2377
  * @param {Object} metadata - Backup metadata
2024
2378
  */
2025
2379
  async delete(backupId, metadata) {
2026
- throw new Error("delete() method must be implemented by subclass");
2380
+ throw new BackupError("delete() method must be implemented by subclass", {
2381
+ operation: "delete",
2382
+ driver: this.constructor.name,
2383
+ backupId,
2384
+ suggestion: "Extend BaseBackupDriver and implement the delete() method"
2385
+ });
2027
2386
  }
2028
2387
  /**
2029
2388
  * List backups available in the destination
@@ -2031,7 +2390,11 @@ class BaseBackupDriver {
2031
2390
  * @returns {Array} List of backup metadata
2032
2391
  */
2033
2392
  async list(options = {}) {
2034
- throw new Error("list() method must be implemented by subclass");
2393
+ throw new BackupError("list() method must be implemented by subclass", {
2394
+ operation: "list",
2395
+ driver: this.constructor.name,
2396
+ suggestion: "Extend BaseBackupDriver and implement the list() method"
2397
+ });
2035
2398
  }
2036
2399
  /**
2037
2400
  * Verify backup integrity
@@ -2041,14 +2404,23 @@ class BaseBackupDriver {
2041
2404
  * @returns {boolean} True if backup is valid
2042
2405
  */
2043
2406
  async verify(backupId, expectedChecksum, metadata) {
2044
- throw new Error("verify() method must be implemented by subclass");
2407
+ throw new BackupError("verify() method must be implemented by subclass", {
2408
+ operation: "verify",
2409
+ driver: this.constructor.name,
2410
+ backupId,
2411
+ suggestion: "Extend BaseBackupDriver and implement the verify() method"
2412
+ });
2045
2413
  }
2046
2414
  /**
2047
2415
  * Get driver type identifier
2048
2416
  * @returns {string} Driver type
2049
2417
  */
2050
2418
  getType() {
2051
- throw new Error("getType() method must be implemented by subclass");
2419
+ throw new BackupError("getType() method must be implemented by subclass", {
2420
+ operation: "getType",
2421
+ driver: this.constructor.name,
2422
+ suggestion: "Extend BaseBackupDriver and implement the getType() method"
2423
+ });
2052
2424
  }
2053
2425
  /**
2054
2426
  * Get driver-specific storage info
@@ -2090,7 +2462,11 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2090
2462
  }
2091
2463
  async onSetup() {
2092
2464
  if (!this.config.path) {
2093
- throw new Error("FilesystemBackupDriver: path configuration is required");
2465
+ throw new BackupError("FilesystemBackupDriver: path configuration is required", {
2466
+ operation: "onSetup",
2467
+ driver: "filesystem",
2468
+ suggestion: 'Provide a path in config: new FilesystemBackupDriver({ path: "/path/to/backups" })'
2469
+ });
2094
2470
  }
2095
2471
  this.log(`Initialized with path: ${this.config.path}`);
2096
2472
  }
@@ -2114,11 +2490,26 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2114
2490
  () => promises.mkdir(targetDir, { recursive: true, mode: this.config.directoryPermissions })
2115
2491
  );
2116
2492
  if (!createDirOk) {
2117
- throw new Error(`Failed to create backup directory: ${createDirErr.message}`);
2493
+ throw new BackupError("Failed to create backup directory", {
2494
+ operation: "upload",
2495
+ driver: "filesystem",
2496
+ backupId,
2497
+ targetDir,
2498
+ original: createDirErr,
2499
+ suggestion: "Check directory permissions and disk space"
2500
+ });
2118
2501
  }
2119
2502
  const [copyOk, copyErr] = await tryFn(() => promises.copyFile(filePath, targetPath));
2120
2503
  if (!copyOk) {
2121
- throw new Error(`Failed to copy backup file: ${copyErr.message}`);
2504
+ throw new BackupError("Failed to copy backup file", {
2505
+ operation: "upload",
2506
+ driver: "filesystem",
2507
+ backupId,
2508
+ filePath,
2509
+ targetPath,
2510
+ original: copyErr,
2511
+ suggestion: "Check file permissions and disk space"
2512
+ });
2122
2513
  }
2123
2514
  const [manifestOk, manifestErr] = await tryFn(
2124
2515
  () => import('fs/promises').then((fs) => fs.writeFile(
@@ -2129,7 +2520,14 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2129
2520
  );
2130
2521
  if (!manifestOk) {
2131
2522
  await tryFn(() => promises.unlink(targetPath));
2132
- throw new Error(`Failed to write manifest: ${manifestErr.message}`);
2523
+ throw new BackupError("Failed to write manifest file", {
2524
+ operation: "upload",
2525
+ driver: "filesystem",
2526
+ backupId,
2527
+ manifestPath,
2528
+ original: manifestErr,
2529
+ suggestion: "Check directory permissions and disk space"
2530
+ });
2133
2531
  }
2134
2532
  const [statOk, , stats] = await tryFn(() => promises.stat(targetPath));
2135
2533
  const size = statOk ? stats.size : 0;
@@ -2148,13 +2546,27 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2148
2546
  );
2149
2547
  const [existsOk] = await tryFn(() => promises.access(sourcePath));
2150
2548
  if (!existsOk) {
2151
- throw new Error(`Backup file not found: ${sourcePath}`);
2549
+ throw new BackupError("Backup file not found", {
2550
+ operation: "download",
2551
+ driver: "filesystem",
2552
+ backupId,
2553
+ sourcePath,
2554
+ suggestion: "Check if backup exists using list() method"
2555
+ });
2152
2556
  }
2153
2557
  const targetDir = path.dirname(targetPath);
2154
2558
  await tryFn(() => promises.mkdir(targetDir, { recursive: true }));
2155
2559
  const [copyOk, copyErr] = await tryFn(() => promises.copyFile(sourcePath, targetPath));
2156
2560
  if (!copyOk) {
2157
- throw new Error(`Failed to download backup: ${copyErr.message}`);
2561
+ throw new BackupError("Failed to download backup", {
2562
+ operation: "download",
2563
+ driver: "filesystem",
2564
+ backupId,
2565
+ sourcePath,
2566
+ targetPath,
2567
+ original: copyErr,
2568
+ suggestion: "Check file permissions and disk space"
2569
+ });
2158
2570
  }
2159
2571
  this.log(`Downloaded backup ${backupId} from ${sourcePath} to ${targetPath}`);
2160
2572
  return targetPath;
@@ -2171,7 +2583,14 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2171
2583
  const [deleteBackupOk] = await tryFn(() => promises.unlink(backupPath));
2172
2584
  const [deleteManifestOk] = await tryFn(() => promises.unlink(manifestPath));
2173
2585
  if (!deleteBackupOk && !deleteManifestOk) {
2174
- throw new Error(`Failed to delete backup files for ${backupId}`);
2586
+ throw new BackupError("Failed to delete backup files", {
2587
+ operation: "delete",
2588
+ driver: "filesystem",
2589
+ backupId,
2590
+ backupPath,
2591
+ manifestPath,
2592
+ suggestion: "Check file permissions"
2593
+ });
2175
2594
  }
2176
2595
  this.log(`Deleted backup ${backupId}`);
2177
2596
  }
@@ -2276,10 +2695,18 @@ class S3BackupDriver extends BaseBackupDriver {
2276
2695
  this.config.bucket = this.database.bucket;
2277
2696
  }
2278
2697
  if (!this.config.client) {
2279
- throw new Error("S3BackupDriver: client is required (either via config or database)");
2698
+ throw new BackupError("S3BackupDriver: client is required", {
2699
+ operation: "onSetup",
2700
+ driver: "s3",
2701
+ suggestion: "Provide a client in config or ensure database has a client configured"
2702
+ });
2280
2703
  }
2281
2704
  if (!this.config.bucket) {
2282
- throw new Error("S3BackupDriver: bucket is required (either via config or database)");
2705
+ throw new BackupError("S3BackupDriver: bucket is required", {
2706
+ operation: "onSetup",
2707
+ driver: "s3",
2708
+ suggestion: "Provide a bucket in config or ensure database has a bucket configured"
2709
+ });
2283
2710
  }
2284
2711
  this.log(`Initialized with bucket: ${this.config.bucket}, path: ${this.config.path}`);
2285
2712
  }
@@ -2321,7 +2748,15 @@ class S3BackupDriver extends BaseBackupDriver {
2321
2748
  });
2322
2749
  });
2323
2750
  if (!uploadOk) {
2324
- throw new Error(`Failed to upload backup file: ${uploadErr.message}`);
2751
+ throw new BackupError("Failed to upload backup file to S3", {
2752
+ operation: "upload",
2753
+ driver: "s3",
2754
+ backupId,
2755
+ bucket: this.config.bucket,
2756
+ key: backupKey,
2757
+ original: uploadErr,
2758
+ suggestion: "Check S3 permissions and bucket configuration"
2759
+ });
2325
2760
  }
2326
2761
  const [manifestOk, manifestErr] = await tryFn(
2327
2762
  () => this.config.client.uploadObject({
@@ -2342,7 +2777,15 @@ class S3BackupDriver extends BaseBackupDriver {
2342
2777
  bucket: this.config.bucket,
2343
2778
  key: backupKey
2344
2779
  }));
2345
- throw new Error(`Failed to upload manifest: ${manifestErr.message}`);
2780
+ throw new BackupError("Failed to upload manifest to S3", {
2781
+ operation: "upload",
2782
+ driver: "s3",
2783
+ backupId,
2784
+ bucket: this.config.bucket,
2785
+ manifestKey,
2786
+ original: manifestErr,
2787
+ suggestion: "Check S3 permissions and bucket configuration"
2788
+ });
2346
2789
  }
2347
2790
  this.log(`Uploaded backup ${backupId} to s3://${this.config.bucket}/${backupKey} (${fileSize} bytes)`);
2348
2791
  return {
@@ -2365,7 +2808,16 @@ class S3BackupDriver extends BaseBackupDriver {
2365
2808
  })
2366
2809
  );
2367
2810
  if (!downloadOk) {
2368
- throw new Error(`Failed to download backup: ${downloadErr.message}`);
2811
+ throw new BackupError("Failed to download backup from S3", {
2812
+ operation: "download",
2813
+ driver: "s3",
2814
+ backupId,
2815
+ bucket: this.config.bucket,
2816
+ key: backupKey,
2817
+ targetPath,
2818
+ original: downloadErr,
2819
+ suggestion: "Check if backup exists and S3 permissions are correct"
2820
+ });
2369
2821
  }
2370
2822
  this.log(`Downloaded backup ${backupId} from s3://${this.config.bucket}/${backupKey} to ${targetPath}`);
2371
2823
  return targetPath;
@@ -2386,7 +2838,15 @@ class S3BackupDriver extends BaseBackupDriver {
2386
2838
  })
2387
2839
  );
2388
2840
  if (!deleteBackupOk && !deleteManifestOk) {
2389
- throw new Error(`Failed to delete backup objects for ${backupId}`);
2841
+ throw new BackupError("Failed to delete backup from S3", {
2842
+ operation: "delete",
2843
+ driver: "s3",
2844
+ backupId,
2845
+ bucket: this.config.bucket,
2846
+ backupKey,
2847
+ manifestKey,
2848
+ suggestion: "Check S3 delete permissions"
2849
+ });
2390
2850
  }
2391
2851
  this.log(`Deleted backup ${backupId} from S3`);
2392
2852
  }
@@ -2499,11 +2959,22 @@ class MultiBackupDriver extends BaseBackupDriver {
2499
2959
  }
2500
2960
  async onSetup() {
2501
2961
  if (!Array.isArray(this.config.destinations) || this.config.destinations.length === 0) {
2502
- throw new Error("MultiBackupDriver: destinations array is required and must not be empty");
2962
+ throw new BackupError("MultiBackupDriver requires non-empty destinations array", {
2963
+ operation: "onSetup",
2964
+ driver: "multi",
2965
+ destinationsProvided: this.config.destinations,
2966
+ suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }, { driver: "filesystem", config: {...} }] }'
2967
+ });
2503
2968
  }
2504
2969
  for (const [index, destConfig] of this.config.destinations.entries()) {
2505
2970
  if (!destConfig.driver) {
2506
- throw new Error(`MultiBackupDriver: destination[${index}] must have a driver type`);
2971
+ throw new BackupError(`Destination ${index} missing driver type`, {
2972
+ operation: "onSetup",
2973
+ driver: "multi",
2974
+ destinationIndex: index,
2975
+ destination: destConfig,
2976
+ suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} } or { driver: "filesystem", config: {...} }'
2977
+ });
2507
2978
  }
2508
2979
  try {
2509
2980
  const driver = createBackupDriver(destConfig.driver, destConfig.config || {});
@@ -2515,7 +2986,15 @@ class MultiBackupDriver extends BaseBackupDriver {
2515
2986
  });
2516
2987
  this.log(`Setup destination ${index}: ${destConfig.driver}`);
2517
2988
  } catch (error) {
2518
- throw new Error(`Failed to setup destination ${index} (${destConfig.driver}): ${error.message}`);
2989
+ throw new BackupError(`Failed to setup destination ${index}`, {
2990
+ operation: "onSetup",
2991
+ driver: "multi",
2992
+ destinationIndex: index,
2993
+ destinationDriver: destConfig.driver,
2994
+ destinationConfig: destConfig.config,
2995
+ original: error,
2996
+ suggestion: "Check destination driver configuration and ensure dependencies are available"
2997
+ });
2519
2998
  }
2520
2999
  }
2521
3000
  if (this.config.requireAll === false) {
@@ -2544,7 +3023,15 @@ class MultiBackupDriver extends BaseBackupDriver {
2544
3023
  this.log(`Priority upload failed to destination ${index}: ${err.message}`);
2545
3024
  }
2546
3025
  }
2547
- throw new Error(`All priority destinations failed: ${errors.map((e) => `${e.destination}: ${e.error}`).join("; ")}`);
3026
+ throw new BackupError("All priority destinations failed", {
3027
+ operation: "upload",
3028
+ driver: "multi",
3029
+ strategy: "priority",
3030
+ backupId,
3031
+ totalDestinations: this.drivers.length,
3032
+ failures: errors,
3033
+ suggestion: "Check destination configurations and ensure at least one destination is accessible"
3034
+ });
2548
3035
  }
2549
3036
  const uploadPromises = this.drivers.map(async ({ driver, config, index }) => {
2550
3037
  const [ok, err, result] = await tryFn(
@@ -2574,10 +3061,28 @@ class MultiBackupDriver extends BaseBackupDriver {
2574
3061
  const successResults = allResults.filter((r) => r.status === "success");
2575
3062
  const failedResults = allResults.filter((r) => r.status === "failed");
2576
3063
  if (strategy === "all" && failedResults.length > 0) {
2577
- throw new Error(`Some destinations failed: ${failedResults.map((r) => `${r.destination}: ${r.error}`).join("; ")}`);
3064
+ throw new BackupError('Some destinations failed with strategy "all"', {
3065
+ operation: "upload",
3066
+ driver: "multi",
3067
+ strategy: "all",
3068
+ backupId,
3069
+ totalDestinations: this.drivers.length,
3070
+ successCount: successResults.length,
3071
+ failedCount: failedResults.length,
3072
+ failures: failedResults,
3073
+ suggestion: 'All destinations must succeed with "all" strategy. Use "any" strategy to tolerate failures, or fix failing destinations.'
3074
+ });
2578
3075
  }
2579
3076
  if (strategy === "any" && successResults.length === 0) {
2580
- throw new Error(`All destinations failed: ${failedResults.map((r) => `${r.destination}: ${r.error}`).join("; ")}`);
3077
+ throw new BackupError('All destinations failed with strategy "any"', {
3078
+ operation: "upload",
3079
+ driver: "multi",
3080
+ strategy: "any",
3081
+ backupId,
3082
+ totalDestinations: this.drivers.length,
3083
+ failures: failedResults,
3084
+ suggestion: 'At least one destination must succeed with "any" strategy. Check all destination configurations.'
3085
+ });
2581
3086
  }
2582
3087
  return allResults;
2583
3088
  }
@@ -2597,7 +3102,14 @@ class MultiBackupDriver extends BaseBackupDriver {
2597
3102
  this.log(`Download failed from destination ${destMetadata.destination}: ${err.message}`);
2598
3103
  }
2599
3104
  }
2600
- throw new Error(`Failed to download backup from any destination`);
3105
+ throw new BackupError("Failed to download backup from any destination", {
3106
+ operation: "download",
3107
+ driver: "multi",
3108
+ backupId,
3109
+ targetPath,
3110
+ attemptedDestinations: destinations.length,
3111
+ suggestion: "Check if backup exists in at least one destination and destinations are accessible"
3112
+ });
2601
3113
  }
2602
3114
  async delete(backupId, metadata) {
2603
3115
  const destinations = Array.isArray(metadata.destinations) ? metadata.destinations : [metadata];
@@ -2619,7 +3131,14 @@ class MultiBackupDriver extends BaseBackupDriver {
2619
3131
  }
2620
3132
  }
2621
3133
  if (successCount === 0 && errors.length > 0) {
2622
- throw new Error(`Failed to delete from any destination: ${errors.join("; ")}`);
3134
+ throw new BackupError("Failed to delete from any destination", {
3135
+ operation: "delete",
3136
+ driver: "multi",
3137
+ backupId,
3138
+ attemptedDestinations: destinations.length,
3139
+ failures: errors,
3140
+ suggestion: "Check if backup exists in destinations and destinations are accessible with delete permissions"
3141
+ });
2623
3142
  }
2624
3143
  if (errors.length > 0) {
2625
3144
  this.log(`Partial delete success, some errors: ${errors.join("; ")}`);
@@ -2719,32 +3238,62 @@ const BACKUP_DRIVERS = {
2719
3238
  function createBackupDriver(driver, config = {}) {
2720
3239
  const DriverClass = BACKUP_DRIVERS[driver];
2721
3240
  if (!DriverClass) {
2722
- throw new Error(`Unknown backup driver: ${driver}. Available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`);
3241
+ throw new BackupError(`Unknown backup driver: ${driver}`, {
3242
+ operation: "createBackupDriver",
3243
+ driver,
3244
+ availableDrivers: Object.keys(BACKUP_DRIVERS),
3245
+ suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`
3246
+ });
2723
3247
  }
2724
3248
  return new DriverClass(config);
2725
3249
  }
2726
3250
  function validateBackupConfig(driver, config = {}) {
2727
3251
  if (!driver || typeof driver !== "string") {
2728
- throw new Error("Driver type must be a non-empty string");
3252
+ throw new BackupError("Driver type must be a non-empty string", {
3253
+ operation: "validateBackupConfig",
3254
+ driver,
3255
+ suggestion: "Provide a valid driver type string (filesystem, s3, or multi)"
3256
+ });
2729
3257
  }
2730
3258
  if (!BACKUP_DRIVERS[driver]) {
2731
- throw new Error(`Unknown backup driver: ${driver}. Available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`);
3259
+ throw new BackupError(`Unknown backup driver: ${driver}`, {
3260
+ operation: "validateBackupConfig",
3261
+ driver,
3262
+ availableDrivers: Object.keys(BACKUP_DRIVERS),
3263
+ suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`
3264
+ });
2732
3265
  }
2733
3266
  switch (driver) {
2734
3267
  case "filesystem":
2735
3268
  if (!config.path) {
2736
- throw new Error('FilesystemBackupDriver requires "path" configuration');
3269
+ throw new BackupError('FilesystemBackupDriver requires "path" configuration', {
3270
+ operation: "validateBackupConfig",
3271
+ driver: "filesystem",
3272
+ config,
3273
+ suggestion: 'Provide a "path" property in config: { path: "/path/to/backups" }'
3274
+ });
2737
3275
  }
2738
3276
  break;
2739
3277
  case "s3":
2740
3278
  break;
2741
3279
  case "multi":
2742
3280
  if (!Array.isArray(config.destinations) || config.destinations.length === 0) {
2743
- throw new Error('MultiBackupDriver requires non-empty "destinations" array');
3281
+ throw new BackupError('MultiBackupDriver requires non-empty "destinations" array', {
3282
+ operation: "validateBackupConfig",
3283
+ driver: "multi",
3284
+ config,
3285
+ suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }] }'
3286
+ });
2744
3287
  }
2745
3288
  config.destinations.forEach((dest, index) => {
2746
3289
  if (!dest.driver) {
2747
- throw new Error(`Destination ${index} must have a "driver" property`);
3290
+ throw new BackupError(`Destination ${index} must have a "driver" property`, {
3291
+ operation: "validateBackupConfig",
3292
+ driver: "multi",
3293
+ destinationIndex: index,
3294
+ destination: dest,
3295
+ suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} }'
3296
+ });
2748
3297
  }
2749
3298
  if (dest.driver !== "multi") {
2750
3299
  validateBackupConfig(dest.driver, dest.config || {});
@@ -3400,6 +3949,36 @@ class BackupPlugin extends Plugin {
3400
3949
  }
3401
3950
  }
3402
3951
 
3952
+ class CacheError extends S3dbError {
3953
+ constructor(message, details = {}) {
3954
+ const { driver = "unknown", operation = "unknown", resourceName, key, ...rest } = details;
3955
+ let description = details.description;
3956
+ if (!description) {
3957
+ description = `
3958
+ Cache Operation Error
3959
+
3960
+ Driver: ${driver}
3961
+ Operation: ${operation}
3962
+ ${resourceName ? `Resource: ${resourceName}` : ""}
3963
+ ${key ? `Key: ${key}` : ""}
3964
+
3965
+ Common causes:
3966
+ 1. Invalid cache key format
3967
+ 2. Cache driver not properly initialized
3968
+ 3. Resource not found or not cached
3969
+ 4. Memory limits exceeded
3970
+ 5. Filesystem permissions issues
3971
+
3972
+ Solution:
3973
+ Check cache configuration and ensure the cache driver is properly initialized.
3974
+
3975
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/cache.md
3976
+ `.trim();
3977
+ }
3978
+ super(message, { ...rest, driver, operation, resourceName, key, description });
3979
+ }
3980
+ }
3981
+
3403
3982
  class Cache extends EventEmitter {
3404
3983
  constructor(config = {}) {
3405
3984
  super();
@@ -3416,7 +3995,13 @@ class Cache extends EventEmitter {
3416
3995
  }
3417
3996
  validateKey(key) {
3418
3997
  if (key === null || key === void 0 || typeof key !== "string" || !key) {
3419
- throw new Error("Invalid key");
3998
+ throw new CacheError("Invalid cache key", {
3999
+ operation: "validateKey",
4000
+ driver: this.constructor.name,
4001
+ key,
4002
+ keyType: typeof key,
4003
+ suggestion: "Cache key must be a non-empty string"
4004
+ });
3420
4005
  }
3421
4006
  }
3422
4007
  // generic class methods
@@ -3503,7 +4088,11 @@ class ResourceReader extends EventEmitter {
3503
4088
  constructor({ resource, batchSize = 10, concurrency = 5 }) {
3504
4089
  super();
3505
4090
  if (!resource) {
3506
- throw new Error("Resource is required for ResourceReader");
4091
+ throw new StreamError("Resource is required for ResourceReader", {
4092
+ operation: "constructor",
4093
+ resource: resource?.name,
4094
+ suggestion: "Pass a valid Resource instance when creating ResourceReader"
4095
+ });
3507
4096
  }
3508
4097
  this.resource = resource;
3509
4098
  this.client = resource.client;
@@ -3627,7 +4216,10 @@ class ResourceWriter extends EventEmitter {
3627
4216
  function streamToString(stream) {
3628
4217
  return new Promise((resolve, reject) => {
3629
4218
  if (!stream) {
3630
- return reject(new Error("streamToString: stream is undefined"));
4219
+ return reject(new StreamError("Stream is undefined", {
4220
+ operation: "streamToString",
4221
+ suggestion: "Ensure a valid stream is passed to streamToString()"
4222
+ }));
3631
4223
  }
3632
4224
  const chunks = [];
3633
4225
  stream.on("data", (chunk) => chunks.push(chunk));
@@ -5121,7 +5713,13 @@ class CachePlugin extends Plugin {
5121
5713
  async warmCache(resourceName, options = {}) {
5122
5714
  const resource = this.database.resources[resourceName];
5123
5715
  if (!resource) {
5124
- throw new Error(`Resource '${resourceName}' not found`);
5716
+ throw new CacheError("Resource not found for cache warming", {
5717
+ operation: "warmCache",
5718
+ driver: this.driver?.constructor.name,
5719
+ resourceName,
5720
+ availableResources: Object.keys(this.database.resources),
5721
+ suggestion: "Check resource name spelling or ensure resource has been created"
5722
+ });
5125
5723
  }
5126
5724
  const { includePartitions = true, sampleSize = 100 } = options;
5127
5725
  if (this.driver instanceof PartitionAwareFilesystemCache && resource.warmPartitionCache) {
@@ -8238,6 +8836,35 @@ class EventualConsistencyPlugin extends Plugin {
8238
8836
  }
8239
8837
  }
8240
8838
 
8839
+ class FulltextError extends S3dbError {
8840
+ constructor(message, details = {}) {
8841
+ const { resourceName, query, operation = "unknown", ...rest } = details;
8842
+ let description = details.description;
8843
+ if (!description) {
8844
+ description = `
8845
+ Fulltext Search Operation Error
8846
+
8847
+ Operation: ${operation}
8848
+ ${resourceName ? `Resource: ${resourceName}` : ""}
8849
+ ${query ? `Query: ${query}` : ""}
8850
+
8851
+ Common causes:
8852
+ 1. Resource not indexed for fulltext search
8853
+ 2. Invalid query syntax
8854
+ 3. Index not built yet
8855
+ 4. Search configuration missing
8856
+ 5. Field not indexed
8857
+
8858
+ Solution:
8859
+ Ensure resource is configured for fulltext search and index is built.
8860
+
8861
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/fulltext.md
8862
+ `.trim();
8863
+ }
8864
+ super(message, { ...rest, resourceName, query, operation, description });
8865
+ }
8866
+ }
8867
+
8241
8868
  class FullTextPlugin extends Plugin {
8242
8869
  constructor(options = {}) {
8243
8870
  super();
@@ -8544,7 +9171,13 @@ class FullTextPlugin extends Plugin {
8544
9171
  }
8545
9172
  const resource = this.database.resources[resourceName];
8546
9173
  if (!resource) {
8547
- throw new Error(`Resource '${resourceName}' not found`);
9174
+ throw new FulltextError(`Resource '${resourceName}' not found`, {
9175
+ operation: "searchRecords",
9176
+ resourceName,
9177
+ query,
9178
+ availableResources: Object.keys(this.database.resources),
9179
+ suggestion: "Check resource name or ensure resource is created before searching"
9180
+ });
8548
9181
  }
8549
9182
  const recordIds = searchResults.map((result2) => result2.recordId);
8550
9183
  const records = await resource.getMany(recordIds);
@@ -8561,7 +9194,12 @@ class FullTextPlugin extends Plugin {
8561
9194
  async rebuildIndex(resourceName) {
8562
9195
  const resource = this.database.resources[resourceName];
8563
9196
  if (!resource) {
8564
- throw new Error(`Resource '${resourceName}' not found`);
9197
+ throw new FulltextError(`Resource '${resourceName}' not found`, {
9198
+ operation: "rebuildIndex",
9199
+ resourceName,
9200
+ availableResources: Object.keys(this.database.resources),
9201
+ suggestion: "Check resource name or ensure resource is created before rebuilding index"
9202
+ });
8565
9203
  }
8566
9204
  for (const [key] of this.indexes.entries()) {
8567
9205
  if (key.startsWith(`${resourceName}:`)) {
@@ -9346,6 +9984,35 @@ function createConsumer(driver, config) {
9346
9984
  return new ConsumerClass(config);
9347
9985
  }
9348
9986
 
9987
+ class QueueError extends S3dbError {
9988
+ constructor(message, details = {}) {
9989
+ const { queueName, operation = "unknown", messageId, ...rest } = details;
9990
+ let description = details.description;
9991
+ if (!description) {
9992
+ description = `
9993
+ Queue Operation Error
9994
+
9995
+ Operation: ${operation}
9996
+ ${queueName ? `Queue: ${queueName}` : ""}
9997
+ ${messageId ? `Message ID: ${messageId}` : ""}
9998
+
9999
+ Common causes:
10000
+ 1. Queue not properly configured
10001
+ 2. Message handler not registered
10002
+ 3. Queue resource not found
10003
+ 4. SQS/RabbitMQ connection failed
10004
+ 5. Message processing timeout
10005
+
10006
+ Solution:
10007
+ Check queue configuration and message handler registration.
10008
+
10009
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue.md
10010
+ `.trim();
10011
+ }
10012
+ super(message, { ...rest, queueName, operation, messageId, description });
10013
+ }
10014
+ }
10015
+
9349
10016
  class QueueConsumerPlugin extends Plugin {
9350
10017
  constructor(options = {}) {
9351
10018
  super(options);
@@ -9406,13 +10073,32 @@ class QueueConsumerPlugin extends Plugin {
9406
10073
  let action = body.action || msg.action;
9407
10074
  let data = body.data || msg.data;
9408
10075
  if (!resource) {
9409
- throw new Error("QueueConsumerPlugin: resource not found in message");
10076
+ throw new QueueError("Resource not found in message", {
10077
+ operation: "handleMessage",
10078
+ queueName: configuredResource,
10079
+ messageBody: body,
10080
+ suggestion: 'Ensure message includes a "resource" field specifying the target resource name'
10081
+ });
9410
10082
  }
9411
10083
  if (!action) {
9412
- throw new Error("QueueConsumerPlugin: action not found in message");
10084
+ throw new QueueError("Action not found in message", {
10085
+ operation: "handleMessage",
10086
+ queueName: configuredResource,
10087
+ resource,
10088
+ messageBody: body,
10089
+ suggestion: 'Ensure message includes an "action" field (insert, update, or delete)'
10090
+ });
9413
10091
  }
9414
10092
  const resourceObj = this.database.resources[resource];
9415
- if (!resourceObj) throw new Error(`QueueConsumerPlugin: resource '${resource}' not found`);
10093
+ if (!resourceObj) {
10094
+ throw new QueueError(`Resource '${resource}' not found`, {
10095
+ operation: "handleMessage",
10096
+ queueName: configuredResource,
10097
+ resource,
10098
+ availableResources: Object.keys(this.database.resources),
10099
+ suggestion: "Check resource name or ensure resource is created before consuming messages"
10100
+ });
10101
+ }
9416
10102
  let result;
9417
10103
  const [ok, err, res] = await tryFn(async () => {
9418
10104
  if (action === "insert") {
@@ -9423,7 +10109,14 @@ class QueueConsumerPlugin extends Plugin {
9423
10109
  } else if (action === "delete") {
9424
10110
  result = await resourceObj.delete(data.id);
9425
10111
  } else {
9426
- throw new Error(`QueueConsumerPlugin: unsupported action '${action}'`);
10112
+ throw new QueueError(`Unsupported action '${action}'`, {
10113
+ operation: "handleMessage",
10114
+ queueName: configuredResource,
10115
+ resource,
10116
+ action,
10117
+ supportedActions: ["insert", "update", "delete"],
10118
+ suggestion: "Use one of the supported actions: insert, update, or delete"
10119
+ });
9427
10120
  }
9428
10121
  return result;
9429
10122
  });
@@ -9436,6 +10129,35 @@ class QueueConsumerPlugin extends Plugin {
9436
10129
  }
9437
10130
  }
9438
10131
 
10132
+ class ReplicationError extends S3dbError {
10133
+ constructor(message, details = {}) {
10134
+ const { replicatorClass = "unknown", operation = "unknown", resourceName, ...rest } = details;
10135
+ let description = details.description;
10136
+ if (!description) {
10137
+ description = `
10138
+ Replication Operation Error
10139
+
10140
+ Replicator: ${replicatorClass}
10141
+ Operation: ${operation}
10142
+ ${resourceName ? `Resource: ${resourceName}` : ""}
10143
+
10144
+ Common causes:
10145
+ 1. Invalid replicator configuration
10146
+ 2. Target system not accessible
10147
+ 3. Resource not configured for replication
10148
+ 4. Invalid operation type
10149
+ 5. Transformation function errors
10150
+
10151
+ Solution:
10152
+ Check replicator configuration and ensure target system is accessible.
10153
+
10154
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md
10155
+ `.trim();
10156
+ }
10157
+ super(message, { ...rest, replicatorClass, operation, resourceName, description });
10158
+ }
10159
+ }
10160
+
9439
10161
  class BaseReplicator extends EventEmitter {
9440
10162
  constructor(config = {}) {
9441
10163
  super();
@@ -9461,7 +10183,12 @@ class BaseReplicator extends EventEmitter {
9461
10183
  * @returns {Promise<Object>} replicator result
9462
10184
  */
9463
10185
  async replicate(resourceName, operation, data, id) {
9464
- throw new Error(`replicate() method must be implemented by ${this.name}`);
10186
+ throw new ReplicationError("replicate() method must be implemented by subclass", {
10187
+ operation: "replicate",
10188
+ replicatorClass: this.name,
10189
+ resourceName,
10190
+ suggestion: "Extend BaseReplicator and implement the replicate() method"
10191
+ });
9465
10192
  }
9466
10193
  /**
9467
10194
  * Replicate multiple records in batch
@@ -9470,14 +10197,24 @@ class BaseReplicator extends EventEmitter {
9470
10197
  * @returns {Promise<Object>} Batch replicator result
9471
10198
  */
9472
10199
  async replicateBatch(resourceName, records) {
9473
- throw new Error(`replicateBatch() method must be implemented by ${this.name}`);
10200
+ throw new ReplicationError("replicateBatch() method must be implemented by subclass", {
10201
+ operation: "replicateBatch",
10202
+ replicatorClass: this.name,
10203
+ resourceName,
10204
+ batchSize: records?.length,
10205
+ suggestion: "Extend BaseReplicator and implement the replicateBatch() method"
10206
+ });
9474
10207
  }
9475
10208
  /**
9476
10209
  * Test the connection to the target
9477
10210
  * @returns {Promise<boolean>} True if connection is successful
9478
10211
  */
9479
10212
  async testConnection() {
9480
- throw new Error(`testConnection() method must be implemented by ${this.name}`);
10213
+ throw new ReplicationError("testConnection() method must be implemented by subclass", {
10214
+ operation: "testConnection",
10215
+ replicatorClass: this.name,
10216
+ suggestion: "Extend BaseReplicator and implement the testConnection() method"
10217
+ });
9481
10218
  }
9482
10219
  /**
9483
10220
  * Get replicator status and statistics
@@ -10649,7 +11386,17 @@ class Client extends EventEmitter {
10649
11386
  });
10650
11387
  this.emit("moveAllObjects", { results, errors }, { prefixFrom, prefixTo });
10651
11388
  if (errors.length > 0) {
10652
- throw new Error("Some objects could not be moved");
11389
+ throw new UnknownError("Some objects could not be moved", {
11390
+ bucket: this.config.bucket,
11391
+ operation: "moveAllObjects",
11392
+ prefixFrom,
11393
+ prefixTo,
11394
+ totalKeys: keys.length,
11395
+ failedCount: errors.length,
11396
+ successCount: results.length,
11397
+ errors: errors.map((e) => ({ message: e.message, raw: e.raw })),
11398
+ suggestion: "Check S3 permissions and retry failed objects individually"
11399
+ });
10653
11400
  }
10654
11401
  return results;
10655
11402
  }
@@ -11363,7 +12110,14 @@ async function handleInsert$4({ resource, data, mappedData, originalData }) {
11363
12110
  }
11364
12111
  });
11365
12112
  if (totalSize > effectiveLimit) {
11366
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
12113
+ throw new MetadataLimitError("Metadata size exceeds 2KB limit on insert", {
12114
+ totalSize,
12115
+ effectiveLimit,
12116
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
12117
+ excess: totalSize - effectiveLimit,
12118
+ resourceName: resource.name,
12119
+ operation: "insert"
12120
+ });
11367
12121
  }
11368
12122
  return { mappedData, body: "" };
11369
12123
  }
@@ -11378,7 +12132,15 @@ async function handleUpdate$4({ resource, id, data, mappedData, originalData })
11378
12132
  }
11379
12133
  });
11380
12134
  if (totalSize > effectiveLimit) {
11381
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
12135
+ throw new MetadataLimitError("Metadata size exceeds 2KB limit on update", {
12136
+ totalSize,
12137
+ effectiveLimit,
12138
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
12139
+ excess: totalSize - effectiveLimit,
12140
+ resourceName: resource.name,
12141
+ operation: "update",
12142
+ id
12143
+ });
11382
12144
  }
11383
12145
  return { mappedData, body: JSON.stringify(mappedData) };
11384
12146
  }
@@ -11393,7 +12155,15 @@ async function handleUpsert$4({ resource, id, data, mappedData }) {
11393
12155
  }
11394
12156
  });
11395
12157
  if (totalSize > effectiveLimit) {
11396
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
12158
+ throw new MetadataLimitError("Metadata size exceeds 2KB limit on upsert", {
12159
+ totalSize,
12160
+ effectiveLimit,
12161
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
12162
+ excess: totalSize - effectiveLimit,
12163
+ resourceName: resource.name,
12164
+ operation: "upsert",
12165
+ id
12166
+ });
11397
12167
  }
11398
12168
  return { mappedData, body: "" };
11399
12169
  }
@@ -11735,7 +12505,11 @@ const behaviors = {
11735
12505
  function getBehavior(behaviorName) {
11736
12506
  const behavior = behaviors[behaviorName];
11737
12507
  if (!behavior) {
11738
- throw new Error(`Unknown behavior: ${behaviorName}. Available behaviors: ${Object.keys(behaviors).join(", ")}`);
12508
+ throw new BehaviorError(`Unknown behavior: ${behaviorName}`, {
12509
+ behavior: behaviorName,
12510
+ availableBehaviors: Object.keys(behaviors),
12511
+ operation: "getBehavior"
12512
+ });
11739
12513
  }
11740
12514
  return behavior;
11741
12515
  }
@@ -14259,7 +15033,7 @@ class Database extends EventEmitter {
14259
15033
  this.id = idGenerator(7);
14260
15034
  this.version = "1";
14261
15035
  this.s3dbVersion = (() => {
14262
- const [ok, err, version] = tryFn(() => true ? "11.2.3" : "latest");
15036
+ const [ok, err, version] = tryFn(() => true ? "11.2.4" : "latest");
14263
15037
  return ok ? version : "latest";
14264
15038
  })();
14265
15039
  this.resources = {};
@@ -14604,7 +15378,12 @@ class Database extends EventEmitter {
14604
15378
  const pluginName = name.toLowerCase().replace("plugin", "");
14605
15379
  const plugin = this.plugins[pluginName] || this.pluginRegistry[pluginName];
14606
15380
  if (!plugin) {
14607
- throw new Error(`Plugin '${name}' not found`);
15381
+ throw new DatabaseError(`Plugin '${name}' not found`, {
15382
+ operation: "uninstallPlugin",
15383
+ pluginName: name,
15384
+ availablePlugins: Object.keys(this.pluginRegistry),
15385
+ suggestion: "Check plugin name or list available plugins using Object.keys(db.pluginRegistry)"
15386
+ });
14608
15387
  }
14609
15388
  if (plugin.stop) {
14610
15389
  await plugin.stop();
@@ -15237,10 +16016,20 @@ class Database extends EventEmitter {
15237
16016
  addHook(event, fn) {
15238
16017
  if (!this._hooks) this._initHooks();
15239
16018
  if (!this._hooks.has(event)) {
15240
- throw new Error(`Unknown hook event: ${event}. Available events: ${this._hookEvents.join(", ")}`);
16019
+ throw new DatabaseError(`Unknown hook event: ${event}`, {
16020
+ operation: "addHook",
16021
+ invalidEvent: event,
16022
+ availableEvents: this._hookEvents,
16023
+ suggestion: `Use one of the available hook events: ${this._hookEvents.join(", ")}`
16024
+ });
15241
16025
  }
15242
16026
  if (typeof fn !== "function") {
15243
- throw new Error("Hook function must be a function");
16027
+ throw new DatabaseError("Hook function must be a function", {
16028
+ operation: "addHook",
16029
+ event,
16030
+ receivedType: typeof fn,
16031
+ suggestion: "Provide a function that will be called when the hook event occurs"
16032
+ });
15244
16033
  }
15245
16034
  this._hooks.get(event).push(fn);
15246
16035
  }
@@ -15378,7 +16167,11 @@ class S3dbReplicator extends BaseReplicator {
15378
16167
  this.targetDatabase = new S3db(targetConfig);
15379
16168
  await this.targetDatabase.connect();
15380
16169
  } else {
15381
- throw new Error("S3dbReplicator: No client or connectionString provided");
16170
+ throw new ReplicationError("S3dbReplicator requires client or connectionString", {
16171
+ operation: "initialize",
16172
+ replicatorClass: "S3dbReplicator",
16173
+ suggestion: 'Provide either a client instance or connectionString in config: { client: db } or { connectionString: "s3://..." }'
16174
+ });
15382
16175
  }
15383
16176
  this.emit("connected", {
15384
16177
  replicator: this.name,
@@ -15409,7 +16202,13 @@ class S3dbReplicator extends BaseReplicator {
15409
16202
  const normResource = normalizeResourceName$1(resource);
15410
16203
  const entry = this.resourcesMap[normResource];
15411
16204
  if (!entry) {
15412
- throw new Error(`[S3dbReplicator] Resource not configured: ${resource}`);
16205
+ throw new ReplicationError("Resource not configured for replication", {
16206
+ operation: "replicate",
16207
+ replicatorClass: "S3dbReplicator",
16208
+ resourceName: resource,
16209
+ configuredResources: Object.keys(this.resourcesMap),
16210
+ suggestion: 'Add resource to replicator resources map: { resources: { [resourceName]: "destination" } }'
16211
+ });
15413
16212
  }
15414
16213
  if (Array.isArray(entry)) {
15415
16214
  const results = [];
@@ -15477,7 +16276,14 @@ class S3dbReplicator extends BaseReplicator {
15477
16276
  } else if (operation === "delete") {
15478
16277
  result = await destResourceObj.delete(recordId);
15479
16278
  } else {
15480
- throw new Error(`Invalid operation: ${operation}. Supported operations are: insert, update, delete`);
16279
+ throw new ReplicationError(`Invalid replication operation: ${operation}`, {
16280
+ operation: "replicate",
16281
+ replicatorClass: "S3dbReplicator",
16282
+ invalidOperation: operation,
16283
+ supportedOperations: ["insert", "update", "delete"],
16284
+ resourceName: sourceResource,
16285
+ suggestion: "Use one of the supported operations: insert, update, delete"
16286
+ });
15481
16287
  }
15482
16288
  return result;
15483
16289
  }
@@ -15545,7 +16351,13 @@ class S3dbReplicator extends BaseReplicator {
15545
16351
  const norm = normalizeResourceName$1(resource);
15546
16352
  const found = available.find((r) => normalizeResourceName$1(r) === norm);
15547
16353
  if (!found) {
15548
- throw new Error(`[S3dbReplicator] Destination resource not found: ${resource}. Available: ${available.join(", ")}`);
16354
+ throw new ReplicationError("Destination resource not found in target database", {
16355
+ operation: "_getDestResourceObj",
16356
+ replicatorClass: "S3dbReplicator",
16357
+ destinationResource: resource,
16358
+ availableResources: available,
16359
+ suggestion: "Create the resource in target database or check resource name spelling"
16360
+ });
15549
16361
  }
15550
16362
  return db.resources[found];
15551
16363
  }
@@ -15594,7 +16406,13 @@ class S3dbReplicator extends BaseReplicator {
15594
16406
  }
15595
16407
  async testConnection() {
15596
16408
  const [ok, err] = await tryFn(async () => {
15597
- if (!this.targetDatabase) throw new Error("No target database configured");
16409
+ if (!this.targetDatabase) {
16410
+ throw new ReplicationError("No target database configured for connection test", {
16411
+ operation: "testConnection",
16412
+ replicatorClass: "S3dbReplicator",
16413
+ suggestion: "Initialize replicator with client or connectionString before testing connection"
16414
+ });
16415
+ }
15598
16416
  if (typeof this.targetDatabase.connect === "function") {
15599
16417
  await this.targetDatabase.connect();
15600
16418
  }
@@ -15981,7 +16799,12 @@ const REPLICATOR_DRIVERS = {
15981
16799
  function createReplicator(driver, config = {}, resources = [], client = null) {
15982
16800
  const ReplicatorClass = REPLICATOR_DRIVERS[driver];
15983
16801
  if (!ReplicatorClass) {
15984
- throw new Error(`Unknown replicator driver: ${driver}. Available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(", ")}`);
16802
+ throw new ReplicationError(`Unknown replicator driver: ${driver}`, {
16803
+ operation: "createReplicator",
16804
+ driver,
16805
+ availableDrivers: Object.keys(REPLICATOR_DRIVERS),
16806
+ suggestion: `Use one of the available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(", ")}`
16807
+ });
15985
16808
  }
15986
16809
  return new ReplicatorClass(config, resources, client);
15987
16810
  }
@@ -15993,12 +16816,40 @@ class ReplicatorPlugin extends Plugin {
15993
16816
  constructor(options = {}) {
15994
16817
  super();
15995
16818
  if (!options.replicators || !Array.isArray(options.replicators)) {
15996
- throw new Error("ReplicatorPlugin: replicators array is required");
16819
+ throw new ReplicationError("ReplicatorPlugin requires replicators array", {
16820
+ operation: "constructor",
16821
+ pluginName: "ReplicatorPlugin",
16822
+ providedOptions: Object.keys(options),
16823
+ suggestion: 'Provide replicators array: new ReplicatorPlugin({ replicators: [{ driver: "s3db", resources: [...] }] })'
16824
+ });
15997
16825
  }
15998
16826
  for (const rep of options.replicators) {
15999
- if (!rep.driver) throw new Error("ReplicatorPlugin: each replicator must have a driver");
16000
- if (!rep.resources || typeof rep.resources !== "object") throw new Error("ReplicatorPlugin: each replicator must have resources config");
16001
- if (Object.keys(rep.resources).length === 0) throw new Error("ReplicatorPlugin: each replicator must have at least one resource configured");
16827
+ if (!rep.driver) {
16828
+ throw new ReplicationError("Each replicator must have a driver", {
16829
+ operation: "constructor",
16830
+ pluginName: "ReplicatorPlugin",
16831
+ replicatorConfig: rep,
16832
+ suggestion: 'Each replicator entry must specify a driver: { driver: "s3db", resources: {...} }'
16833
+ });
16834
+ }
16835
+ if (!rep.resources || typeof rep.resources !== "object") {
16836
+ throw new ReplicationError("Each replicator must have resources config", {
16837
+ operation: "constructor",
16838
+ pluginName: "ReplicatorPlugin",
16839
+ driver: rep.driver,
16840
+ replicatorConfig: rep,
16841
+ suggestion: 'Provide resources as object or array: { driver: "s3db", resources: ["users"] } or { resources: { users: "people" } }'
16842
+ });
16843
+ }
16844
+ if (Object.keys(rep.resources).length === 0) {
16845
+ throw new ReplicationError("Each replicator must have at least one resource configured", {
16846
+ operation: "constructor",
16847
+ pluginName: "ReplicatorPlugin",
16848
+ driver: rep.driver,
16849
+ replicatorConfig: rep,
16850
+ suggestion: 'Add at least one resource to replicate: { driver: "s3db", resources: ["users"] }'
16851
+ });
16852
+ }
16002
16853
  }
16003
16854
  this.config = {
16004
16855
  replicators: options.replicators || [],
@@ -16424,7 +17275,13 @@ class ReplicatorPlugin extends Plugin {
16424
17275
  async syncAllData(replicatorId) {
16425
17276
  const replicator = this.replicators.find((r) => r.id === replicatorId);
16426
17277
  if (!replicator) {
16427
- throw new Error(`Replicator not found: ${replicatorId}`);
17278
+ throw new ReplicationError("Replicator not found", {
17279
+ operation: "syncAllData",
17280
+ pluginName: "ReplicatorPlugin",
17281
+ replicatorId,
17282
+ availableReplicators: this.replicators.map((r) => r.id),
17283
+ suggestion: "Check replicator ID or use getReplicatorStats() to list available replicators"
17284
+ });
16428
17285
  }
16429
17286
  this.stats.lastSync = (/* @__PURE__ */ new Date()).toISOString();
16430
17287
  for (const resourceName in this.database.resources) {
@@ -16954,6 +17811,35 @@ class S3QueuePlugin extends Plugin {
16954
17811
  }
16955
17812
  }
16956
17813
 
17814
+ class SchedulerError extends S3dbError {
17815
+ constructor(message, details = {}) {
17816
+ const { taskId, operation = "unknown", cronExpression, ...rest } = details;
17817
+ let description = details.description;
17818
+ if (!description) {
17819
+ description = `
17820
+ Scheduler Operation Error
17821
+
17822
+ Operation: ${operation}
17823
+ ${taskId ? `Task ID: ${taskId}` : ""}
17824
+ ${cronExpression ? `Cron: ${cronExpression}` : ""}
17825
+
17826
+ Common causes:
17827
+ 1. Invalid cron expression format
17828
+ 2. Task not found or already exists
17829
+ 3. Scheduler not properly initialized
17830
+ 4. Job execution failure
17831
+ 5. Resource conflicts
17832
+
17833
+ Solution:
17834
+ Check task configuration and ensure scheduler is properly initialized.
17835
+
17836
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/scheduler.md
17837
+ `.trim();
17838
+ }
17839
+ super(message, { ...rest, taskId, operation, cronExpression, description });
17840
+ }
17841
+ }
17842
+
16957
17843
  class SchedulerPlugin extends Plugin {
16958
17844
  constructor(options = {}) {
16959
17845
  super();
@@ -16987,17 +17873,36 @@ class SchedulerPlugin extends Plugin {
16987
17873
  }
16988
17874
  _validateConfiguration() {
16989
17875
  if (Object.keys(this.config.jobs).length === 0) {
16990
- throw new Error("SchedulerPlugin: At least one job must be defined");
17876
+ throw new SchedulerError("At least one job must be defined", {
17877
+ operation: "validateConfiguration",
17878
+ jobCount: 0,
17879
+ suggestion: 'Provide at least one job in the jobs configuration: { jobs: { myJob: { schedule: "* * * * *", action: async () => {...} } } }'
17880
+ });
16991
17881
  }
16992
17882
  for (const [jobName, job] of Object.entries(this.config.jobs)) {
16993
17883
  if (!job.schedule) {
16994
- throw new Error(`SchedulerPlugin: Job '${jobName}' must have a schedule`);
17884
+ throw new SchedulerError(`Job '${jobName}' must have a schedule`, {
17885
+ operation: "validateConfiguration",
17886
+ taskId: jobName,
17887
+ providedConfig: Object.keys(job),
17888
+ suggestion: 'Add a schedule property with a valid cron expression: { schedule: "0 * * * *", action: async () => {...} }'
17889
+ });
16995
17890
  }
16996
17891
  if (!job.action || typeof job.action !== "function") {
16997
- throw new Error(`SchedulerPlugin: Job '${jobName}' must have an action function`);
17892
+ throw new SchedulerError(`Job '${jobName}' must have an action function`, {
17893
+ operation: "validateConfiguration",
17894
+ taskId: jobName,
17895
+ actionType: typeof job.action,
17896
+ suggestion: 'Provide an action function: { schedule: "...", action: async (db, ctx) => {...} }'
17897
+ });
16998
17898
  }
16999
17899
  if (!this._isValidCronExpression(job.schedule)) {
17000
- throw new Error(`SchedulerPlugin: Job '${jobName}' has invalid cron expression: ${job.schedule}`);
17900
+ throw new SchedulerError(`Job '${jobName}' has invalid cron expression`, {
17901
+ operation: "validateConfiguration",
17902
+ taskId: jobName,
17903
+ cronExpression: job.schedule,
17904
+ suggestion: "Use valid cron format (5 fields: minute hour day month weekday) or shortcuts (@hourly, @daily, @weekly, @monthly, @yearly)"
17905
+ });
17001
17906
  }
17002
17907
  }
17003
17908
  }
@@ -17295,10 +18200,20 @@ class SchedulerPlugin extends Plugin {
17295
18200
  async runJob(jobName, context = {}) {
17296
18201
  const job = this.jobs.get(jobName);
17297
18202
  if (!job) {
17298
- throw new Error(`Job '${jobName}' not found`);
18203
+ throw new SchedulerError(`Job '${jobName}' not found`, {
18204
+ operation: "runJob",
18205
+ taskId: jobName,
18206
+ availableJobs: Array.from(this.jobs.keys()),
18207
+ suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
18208
+ });
17299
18209
  }
17300
18210
  if (this.activeJobs.has(jobName)) {
17301
- throw new Error(`Job '${jobName}' is already running`);
18211
+ throw new SchedulerError(`Job '${jobName}' is already running`, {
18212
+ operation: "runJob",
18213
+ taskId: jobName,
18214
+ executionId: this.activeJobs.get(jobName),
18215
+ suggestion: "Wait for current execution to complete or check job status with getJobStatus()"
18216
+ });
17302
18217
  }
17303
18218
  await this._executeJob(jobName);
17304
18219
  }
@@ -17308,7 +18223,12 @@ class SchedulerPlugin extends Plugin {
17308
18223
  enableJob(jobName) {
17309
18224
  const job = this.jobs.get(jobName);
17310
18225
  if (!job) {
17311
- throw new Error(`Job '${jobName}' not found`);
18226
+ throw new SchedulerError(`Job '${jobName}' not found`, {
18227
+ operation: "enableJob",
18228
+ taskId: jobName,
18229
+ availableJobs: Array.from(this.jobs.keys()),
18230
+ suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
18231
+ });
17312
18232
  }
17313
18233
  job.enabled = true;
17314
18234
  this._scheduleNextExecution(jobName);
@@ -17320,7 +18240,12 @@ class SchedulerPlugin extends Plugin {
17320
18240
  disableJob(jobName) {
17321
18241
  const job = this.jobs.get(jobName);
17322
18242
  if (!job) {
17323
- throw new Error(`Job '${jobName}' not found`);
18243
+ throw new SchedulerError(`Job '${jobName}' not found`, {
18244
+ operation: "disableJob",
18245
+ taskId: jobName,
18246
+ availableJobs: Array.from(this.jobs.keys()),
18247
+ suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
18248
+ });
17324
18249
  }
17325
18250
  job.enabled = false;
17326
18251
  const timer = this.timers.get(jobName);
@@ -17419,13 +18344,28 @@ class SchedulerPlugin extends Plugin {
17419
18344
  */
17420
18345
  addJob(jobName, jobConfig) {
17421
18346
  if (this.jobs.has(jobName)) {
17422
- throw new Error(`Job '${jobName}' already exists`);
18347
+ throw new SchedulerError(`Job '${jobName}' already exists`, {
18348
+ operation: "addJob",
18349
+ taskId: jobName,
18350
+ existingJobs: Array.from(this.jobs.keys()),
18351
+ suggestion: "Use a different job name or remove the existing job first with removeJob()"
18352
+ });
17423
18353
  }
17424
18354
  if (!jobConfig.schedule || !jobConfig.action) {
17425
- throw new Error("Job must have schedule and action");
18355
+ throw new SchedulerError("Job must have schedule and action", {
18356
+ operation: "addJob",
18357
+ taskId: jobName,
18358
+ providedConfig: Object.keys(jobConfig),
18359
+ suggestion: 'Provide both schedule and action: { schedule: "0 * * * *", action: async (db, ctx) => {...} }'
18360
+ });
17426
18361
  }
17427
18362
  if (!this._isValidCronExpression(jobConfig.schedule)) {
17428
- throw new Error(`Invalid cron expression: ${jobConfig.schedule}`);
18363
+ throw new SchedulerError("Invalid cron expression", {
18364
+ operation: "addJob",
18365
+ taskId: jobName,
18366
+ cronExpression: jobConfig.schedule,
18367
+ suggestion: "Use valid cron format (5 fields) or shortcuts (@hourly, @daily, @weekly, @monthly, @yearly)"
18368
+ });
17429
18369
  }
17430
18370
  const job = {
17431
18371
  ...jobConfig,
@@ -17459,7 +18399,12 @@ class SchedulerPlugin extends Plugin {
17459
18399
  removeJob(jobName) {
17460
18400
  const job = this.jobs.get(jobName);
17461
18401
  if (!job) {
17462
- throw new Error(`Job '${jobName}' not found`);
18402
+ throw new SchedulerError(`Job '${jobName}' not found`, {
18403
+ operation: "removeJob",
18404
+ taskId: jobName,
18405
+ availableJobs: Array.from(this.jobs.keys()),
18406
+ suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
18407
+ });
17463
18408
  }
17464
18409
  const timer = this.timers.get(jobName);
17465
18410
  if (timer) {
@@ -17513,6 +18458,36 @@ class SchedulerPlugin extends Plugin {
17513
18458
  }
17514
18459
  }
17515
18460
 
18461
+ class StateMachineError extends S3dbError {
18462
+ constructor(message, details = {}) {
18463
+ const { currentState, targetState, resourceName, operation = "unknown", ...rest } = details;
18464
+ let description = details.description;
18465
+ if (!description) {
18466
+ description = `
18467
+ State Machine Operation Error
18468
+
18469
+ Operation: ${operation}
18470
+ ${currentState ? `Current State: ${currentState}` : ""}
18471
+ ${targetState ? `Target State: ${targetState}` : ""}
18472
+ ${resourceName ? `Resource: ${resourceName}` : ""}
18473
+
18474
+ Common causes:
18475
+ 1. Invalid state transition
18476
+ 2. State machine not configured
18477
+ 3. Transition conditions not met
18478
+ 4. State not defined in configuration
18479
+ 5. Missing transition handler
18480
+
18481
+ Solution:
18482
+ Check state machine configuration and valid transitions.
18483
+
18484
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/state-machine.md
18485
+ `.trim();
18486
+ }
18487
+ super(message, { ...rest, currentState, targetState, resourceName, operation, description });
18488
+ }
18489
+ }
18490
+
17516
18491
  class StateMachinePlugin extends Plugin {
17517
18492
  constructor(options = {}) {
17518
18493
  super();
@@ -17533,17 +18508,36 @@ class StateMachinePlugin extends Plugin {
17533
18508
  }
17534
18509
  _validateConfiguration() {
17535
18510
  if (!this.config.stateMachines || Object.keys(this.config.stateMachines).length === 0) {
17536
- throw new Error("StateMachinePlugin: At least one state machine must be defined");
18511
+ throw new StateMachineError("At least one state machine must be defined", {
18512
+ operation: "validateConfiguration",
18513
+ machineCount: 0,
18514
+ suggestion: "Provide at least one state machine in the stateMachines configuration"
18515
+ });
17537
18516
  }
17538
18517
  for (const [machineName, machine] of Object.entries(this.config.stateMachines)) {
17539
18518
  if (!machine.states || Object.keys(machine.states).length === 0) {
17540
- throw new Error(`StateMachinePlugin: Machine '${machineName}' must have states defined`);
18519
+ throw new StateMachineError(`Machine '${machineName}' must have states defined`, {
18520
+ operation: "validateConfiguration",
18521
+ machineId: machineName,
18522
+ suggestion: "Define at least one state in the states configuration"
18523
+ });
17541
18524
  }
17542
18525
  if (!machine.initialState) {
17543
- throw new Error(`StateMachinePlugin: Machine '${machineName}' must have an initialState`);
18526
+ throw new StateMachineError(`Machine '${machineName}' must have an initialState`, {
18527
+ operation: "validateConfiguration",
18528
+ machineId: machineName,
18529
+ availableStates: Object.keys(machine.states),
18530
+ suggestion: "Specify an initialState property matching one of the defined states"
18531
+ });
17544
18532
  }
17545
18533
  if (!machine.states[machine.initialState]) {
17546
- throw new Error(`StateMachinePlugin: Initial state '${machine.initialState}' not found in machine '${machineName}'`);
18534
+ throw new StateMachineError(`Initial state '${machine.initialState}' not found in machine '${machineName}'`, {
18535
+ operation: "validateConfiguration",
18536
+ machineId: machineName,
18537
+ initialState: machine.initialState,
18538
+ availableStates: Object.keys(machine.states),
18539
+ suggestion: "Set initialState to one of the defined states"
18540
+ });
17547
18541
  }
17548
18542
  }
17549
18543
  }
@@ -17600,12 +18594,25 @@ class StateMachinePlugin extends Plugin {
17600
18594
  async send(machineId, entityId, event, context = {}) {
17601
18595
  const machine = this.machines.get(machineId);
17602
18596
  if (!machine) {
17603
- throw new Error(`State machine '${machineId}' not found`);
18597
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18598
+ operation: "send",
18599
+ machineId,
18600
+ availableMachines: Array.from(this.machines.keys()),
18601
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18602
+ });
17604
18603
  }
17605
18604
  const currentState = await this.getState(machineId, entityId);
17606
18605
  const stateConfig = machine.config.states[currentState];
17607
18606
  if (!stateConfig || !stateConfig.on || !stateConfig.on[event]) {
17608
- throw new Error(`Event '${event}' not valid for state '${currentState}' in machine '${machineId}'`);
18607
+ throw new StateMachineError(`Event '${event}' not valid for state '${currentState}' in machine '${machineId}'`, {
18608
+ operation: "send",
18609
+ machineId,
18610
+ entityId,
18611
+ event,
18612
+ currentState,
18613
+ validEvents: stateConfig && stateConfig.on ? Object.keys(stateConfig.on) : [],
18614
+ suggestion: "Use getValidEvents() to check which events are valid for the current state"
18615
+ });
17609
18616
  }
17610
18617
  const targetState = stateConfig.on[event];
17611
18618
  if (stateConfig.guards && stateConfig.guards[event]) {
@@ -17616,7 +18623,16 @@ class StateMachinePlugin extends Plugin {
17616
18623
  () => guard(context, event, { database: this.database, machineId, entityId })
17617
18624
  );
17618
18625
  if (!guardOk || !guardResult) {
17619
- throw new Error(`Transition blocked by guard '${guardName}': ${guardErr?.message || "Guard returned false"}`);
18626
+ throw new StateMachineError(`Transition blocked by guard '${guardName}'`, {
18627
+ operation: "send",
18628
+ machineId,
18629
+ entityId,
18630
+ event,
18631
+ currentState,
18632
+ guardName,
18633
+ guardError: guardErr?.message || "Guard returned false",
18634
+ suggestion: "Check guard conditions or modify the context to satisfy guard requirements"
18635
+ });
17620
18636
  }
17621
18637
  }
17622
18638
  }
@@ -17726,7 +18742,12 @@ class StateMachinePlugin extends Plugin {
17726
18742
  async getState(machineId, entityId) {
17727
18743
  const machine = this.machines.get(machineId);
17728
18744
  if (!machine) {
17729
- throw new Error(`State machine '${machineId}' not found`);
18745
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18746
+ operation: "getState",
18747
+ machineId,
18748
+ availableMachines: Array.from(this.machines.keys()),
18749
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18750
+ });
17730
18751
  }
17731
18752
  if (machine.currentStates.has(entityId)) {
17732
18753
  return machine.currentStates.get(entityId);
@@ -17752,7 +18773,12 @@ class StateMachinePlugin extends Plugin {
17752
18773
  async getValidEvents(machineId, stateOrEntityId) {
17753
18774
  const machine = this.machines.get(machineId);
17754
18775
  if (!machine) {
17755
- throw new Error(`State machine '${machineId}' not found`);
18776
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18777
+ operation: "getValidEvents",
18778
+ machineId,
18779
+ availableMachines: Array.from(this.machines.keys()),
18780
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18781
+ });
17756
18782
  }
17757
18783
  let state;
17758
18784
  if (machine.config.states[stateOrEntityId]) {
@@ -17801,7 +18827,12 @@ class StateMachinePlugin extends Plugin {
17801
18827
  async initializeEntity(machineId, entityId, context = {}) {
17802
18828
  const machine = this.machines.get(machineId);
17803
18829
  if (!machine) {
17804
- throw new Error(`State machine '${machineId}' not found`);
18830
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18831
+ operation: "initializeEntity",
18832
+ machineId,
18833
+ availableMachines: Array.from(this.machines.keys()),
18834
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18835
+ });
17805
18836
  }
17806
18837
  const initialState = machine.config.initialState;
17807
18838
  machine.currentStates.set(entityId, initialState);
@@ -17820,7 +18851,14 @@ class StateMachinePlugin extends Plugin {
17820
18851
  })
17821
18852
  );
17822
18853
  if (!ok && err && !err.message?.includes("already exists")) {
17823
- throw new Error(`Failed to initialize entity state: ${err.message}`);
18854
+ throw new StateMachineError("Failed to initialize entity state", {
18855
+ operation: "initializeEntity",
18856
+ machineId,
18857
+ entityId,
18858
+ initialState,
18859
+ original: err,
18860
+ suggestion: "Check state resource configuration and database permissions"
18861
+ });
17824
18862
  }
17825
18863
  }
17826
18864
  const initialStateConfig = machine.config.states[initialState];
@@ -17849,7 +18887,12 @@ class StateMachinePlugin extends Plugin {
17849
18887
  visualize(machineId) {
17850
18888
  const machine = this.machines.get(machineId);
17851
18889
  if (!machine) {
17852
- throw new Error(`State machine '${machineId}' not found`);
18890
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18891
+ operation: "visualize",
18892
+ machineId,
18893
+ availableMachines: Array.from(this.machines.keys()),
18894
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18895
+ });
17853
18896
  }
17854
18897
  let dot = `digraph ${machineId} {
17855
18898
  `;
@@ -17899,6 +18942,7 @@ exports.AuditPlugin = AuditPlugin;
17899
18942
  exports.AuthenticationError = AuthenticationError;
17900
18943
  exports.BackupPlugin = BackupPlugin;
17901
18944
  exports.BaseError = BaseError;
18945
+ exports.BehaviorError = BehaviorError;
17902
18946
  exports.CachePlugin = CachePlugin;
17903
18947
  exports.Client = Client;
17904
18948
  exports.ConnectionString = ConnectionString;
@@ -17913,15 +18957,19 @@ exports.ErrorMap = ErrorMap;
17913
18957
  exports.EventualConsistencyPlugin = EventualConsistencyPlugin;
17914
18958
  exports.FullTextPlugin = FullTextPlugin;
17915
18959
  exports.InvalidResourceItem = InvalidResourceItem;
18960
+ exports.MetadataLimitError = MetadataLimitError;
17916
18961
  exports.MetricsPlugin = MetricsPlugin;
17917
18962
  exports.MissingMetadata = MissingMetadata;
17918
18963
  exports.NoSuchBucket = NoSuchBucket;
17919
18964
  exports.NoSuchKey = NoSuchKey;
17920
18965
  exports.NotFound = NotFound;
18966
+ exports.PartitionDriverError = PartitionDriverError;
17921
18967
  exports.PartitionError = PartitionError;
17922
18968
  exports.PermissionError = PermissionError;
17923
18969
  exports.Plugin = Plugin;
18970
+ exports.PluginError = PluginError;
17924
18971
  exports.PluginObject = PluginObject;
18972
+ exports.PluginStorageError = PluginStorageError;
17925
18973
  exports.QueueConsumerPlugin = QueueConsumerPlugin;
17926
18974
  exports.ReplicatorPlugin = ReplicatorPlugin;
17927
18975
  exports.Resource = Resource;
@@ -17938,6 +18986,7 @@ exports.SchedulerPlugin = SchedulerPlugin;
17938
18986
  exports.Schema = Schema;
17939
18987
  exports.SchemaError = SchemaError;
17940
18988
  exports.StateMachinePlugin = StateMachinePlugin;
18989
+ exports.StreamError = StreamError;
17941
18990
  exports.UnknownError = UnknownError;
17942
18991
  exports.ValidationError = ValidationError;
17943
18992
  exports.Validator = Validator;