s3db.js 13.1.0 → 13.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/s3db.cjs.js +1249 -271
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1249 -271
- package/dist/s3db.es.js.map +1 -1
- package/package.json +2 -1
- package/src/clients/memory-client.class.js +16 -16
- package/src/clients/s3-client.class.js +17 -17
- package/src/concerns/error-classifier.js +204 -0
- package/src/database.class.js +9 -9
- package/src/plugins/backup.plugin.js +8 -8
- package/src/plugins/cache.plugin.js +3 -3
- package/src/plugins/concerns/plugin-dependencies.js +12 -0
- package/src/plugins/geo.plugin.js +2 -2
- package/src/plugins/ml.plugin.js +337 -137
- package/src/plugins/relation.plugin.js +1 -1
- package/src/plugins/replicator.plugin.js +16 -16
- package/src/plugins/s3-queue.plugin.js +5 -5
- package/src/plugins/scheduler.plugin.js +7 -7
- package/src/plugins/state-machine.errors.js +9 -1
- package/src/plugins/state-machine.plugin.js +671 -16
- package/src/plugins/ttl.plugin.js +4 -4
- package/src/plugins/vector.plugin.js +10 -10
- package/src/resource.class.js +189 -40
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s3db.js",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.2.2",
|
|
4
4
|
"description": "Use AWS S3, the world's most reliable document storage, as a database with this ORM.",
|
|
5
5
|
"main": "dist/s3db.cjs.js",
|
|
6
6
|
"module": "dist/s3db.es.js",
|
|
@@ -133,6 +133,7 @@
|
|
|
133
133
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
134
134
|
"@rollup/plugin-replace": "^6.0.2",
|
|
135
135
|
"@rollup/plugin-terser": "^0.4.4",
|
|
136
|
+
"@tensorflow/tfjs-node": "^4.22.0",
|
|
136
137
|
"@types/node": "24.7.0",
|
|
137
138
|
"@xenova/transformers": "^2.17.2",
|
|
138
139
|
"@yao-pkg/pkg": "^6.9.0",
|
|
@@ -73,7 +73,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
73
73
|
const commandName = command.constructor.name;
|
|
74
74
|
const input = command.input || {};
|
|
75
75
|
|
|
76
|
-
this.emit('
|
|
76
|
+
this.emit('cl:request', commandName, input);
|
|
77
77
|
|
|
78
78
|
let response;
|
|
79
79
|
|
|
@@ -105,7 +105,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
105
105
|
throw new Error(`Unsupported command: ${commandName}`);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
this.emit('
|
|
108
|
+
this.emit('cl:response', commandName, response, input);
|
|
109
109
|
return response;
|
|
110
110
|
|
|
111
111
|
} catch (error) {
|
|
@@ -237,7 +237,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
237
237
|
ifMatch
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
-
this.emit('
|
|
240
|
+
this.emit('cl:PutObject', null, { key, metadata, contentType, body, contentEncoding, contentLength });
|
|
241
241
|
|
|
242
242
|
return response;
|
|
243
243
|
}
|
|
@@ -257,7 +257,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
this.emit('
|
|
260
|
+
this.emit('cl:GetObject', null, { key });
|
|
261
261
|
|
|
262
262
|
return {
|
|
263
263
|
...response,
|
|
@@ -280,7 +280,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
this.emit('
|
|
283
|
+
this.emit('cl:HeadObject', null, { key });
|
|
284
284
|
|
|
285
285
|
return {
|
|
286
286
|
...response,
|
|
@@ -311,7 +311,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
311
311
|
contentType
|
|
312
312
|
});
|
|
313
313
|
|
|
314
|
-
this.emit('
|
|
314
|
+
this.emit('cl:CopyObject', null, { from, to, metadata, metadataDirective });
|
|
315
315
|
|
|
316
316
|
return response;
|
|
317
317
|
}
|
|
@@ -331,7 +331,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
331
331
|
const fullKey = this.keyPrefix ? path.join(this.keyPrefix, key) : key;
|
|
332
332
|
const response = await this.storage.delete(fullKey);
|
|
333
333
|
|
|
334
|
-
this.emit('
|
|
334
|
+
this.emit('cl:DeleteObject', null, { key });
|
|
335
335
|
|
|
336
336
|
return response;
|
|
337
337
|
}
|
|
@@ -380,7 +380,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
380
380
|
continuationToken
|
|
381
381
|
});
|
|
382
382
|
|
|
383
|
-
this.emit('
|
|
383
|
+
this.emit('cl:ListObjects', null, { prefix, count: response.Contents.length });
|
|
384
384
|
|
|
385
385
|
return response;
|
|
386
386
|
}
|
|
@@ -431,7 +431,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
431
431
|
.map(x => (x.startsWith('/') ? x.replace('/', '') : x));
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
-
this.emit('
|
|
434
|
+
this.emit('cl:GetKeysPage', keys, params);
|
|
435
435
|
return keys;
|
|
436
436
|
}
|
|
437
437
|
|
|
@@ -454,7 +454,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
454
454
|
.map(x => (x.startsWith('/') ? x.replace('/', '') : x));
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
this.emit('
|
|
457
|
+
this.emit('cl:GetAllKeys', keys, { prefix });
|
|
458
458
|
return keys;
|
|
459
459
|
}
|
|
460
460
|
|
|
@@ -464,7 +464,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
464
464
|
async count({ prefix = '' } = {}) {
|
|
465
465
|
const keys = await this.getAllKeys({ prefix });
|
|
466
466
|
const count = keys.length;
|
|
467
|
-
this.emit('
|
|
467
|
+
this.emit('cl:Count', count, { prefix });
|
|
468
468
|
return count;
|
|
469
469
|
}
|
|
470
470
|
|
|
@@ -479,14 +479,14 @@ export class MemoryClient extends EventEmitter {
|
|
|
479
479
|
const result = await this.deleteObjects(keys);
|
|
480
480
|
totalDeleted = result.Deleted.length;
|
|
481
481
|
|
|
482
|
-
this.emit('
|
|
482
|
+
this.emit('cl:DeleteAll', {
|
|
483
483
|
prefix,
|
|
484
484
|
batch: totalDeleted,
|
|
485
485
|
total: totalDeleted
|
|
486
486
|
});
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
-
this.emit('
|
|
489
|
+
this.emit('cl:DeleteAllComplete', {
|
|
490
490
|
prefix,
|
|
491
491
|
totalDeleted
|
|
492
492
|
});
|
|
@@ -504,13 +504,13 @@ export class MemoryClient extends EventEmitter {
|
|
|
504
504
|
|
|
505
505
|
// If offset is beyond available keys, return null
|
|
506
506
|
if (offset >= keys.length) {
|
|
507
|
-
this.emit('
|
|
507
|
+
this.emit('cl:GetContinuationTokenAfterOffset', null, { prefix, offset });
|
|
508
508
|
return null;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
// Return the key at offset position as continuation token
|
|
512
512
|
const token = keys[offset];
|
|
513
|
-
this.emit('
|
|
513
|
+
this.emit('cl:GetContinuationTokenAfterOffset', token, { prefix, offset });
|
|
514
514
|
return token;
|
|
515
515
|
}
|
|
516
516
|
|
|
@@ -544,7 +544,7 @@ export class MemoryClient extends EventEmitter {
|
|
|
544
544
|
}
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
-
this.emit('
|
|
547
|
+
this.emit('cl:MoveAllObjects', { results, errors }, { prefixFrom, prefixTo });
|
|
548
548
|
|
|
549
549
|
if (errors.length > 0) {
|
|
550
550
|
const error = new Error('Some objects could not be moved');
|
|
@@ -100,7 +100,7 @@ export class S3Client extends EventEmitter {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
async sendCommand(command) {
|
|
103
|
-
this.emit("
|
|
103
|
+
this.emit("cl:request", command.constructor.name, command.input);
|
|
104
104
|
const [ok, err, response] = await tryFn(() => this.client.send(command));
|
|
105
105
|
if (!ok) {
|
|
106
106
|
const bucket = this.config.bucket;
|
|
@@ -112,7 +112,7 @@ export class S3Client extends EventEmitter {
|
|
|
112
112
|
commandInput: command.input,
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
|
-
this.emit("
|
|
115
|
+
this.emit("cl:response", command.constructor.name, response, command.input);
|
|
116
116
|
return response;
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -146,7 +146,7 @@ export class S3Client extends EventEmitter {
|
|
|
146
146
|
if (ifMatch !== undefined) options.IfMatch = ifMatch
|
|
147
147
|
|
|
148
148
|
const [ok, err, response] = await tryFn(() => this.sendCommand(new PutObjectCommand(options)));
|
|
149
|
-
this.emit('
|
|
149
|
+
this.emit('cl:PutObject', err || response, { key, metadata, contentType, body, contentEncoding, contentLength });
|
|
150
150
|
|
|
151
151
|
if (!ok) {
|
|
152
152
|
throw mapAwsError(err, {
|
|
@@ -182,7 +182,7 @@ export class S3Client extends EventEmitter {
|
|
|
182
182
|
return res;
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
-
this.emit('
|
|
185
|
+
this.emit('cl:GetObject', err || response, { key });
|
|
186
186
|
|
|
187
187
|
if (!ok) {
|
|
188
188
|
throw mapAwsError(err, {
|
|
@@ -218,7 +218,7 @@ export class S3Client extends EventEmitter {
|
|
|
218
218
|
return res;
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
-
this.emit('
|
|
221
|
+
this.emit('cl:HeadObject', err || response, { key });
|
|
222
222
|
|
|
223
223
|
if (!ok) {
|
|
224
224
|
throw mapAwsError(err, {
|
|
@@ -261,7 +261,7 @@ export class S3Client extends EventEmitter {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
const [ok, err, response] = await tryFn(() => this.sendCommand(new CopyObjectCommand(options)));
|
|
264
|
-
this.emit('
|
|
264
|
+
this.emit('cl:CopyObject', err || response, { from, to, metadataDirective });
|
|
265
265
|
|
|
266
266
|
if (!ok) {
|
|
267
267
|
throw mapAwsError(err, {
|
|
@@ -291,7 +291,7 @@ export class S3Client extends EventEmitter {
|
|
|
291
291
|
};
|
|
292
292
|
|
|
293
293
|
const [ok, err, response] = await tryFn(() => this.sendCommand(new DeleteObjectCommand(options)));
|
|
294
|
-
this.emit('
|
|
294
|
+
this.emit('cl:DeleteObject', err || response, { key });
|
|
295
295
|
|
|
296
296
|
if (!ok) {
|
|
297
297
|
throw mapAwsError(err, {
|
|
@@ -346,7 +346,7 @@ export class S3Client extends EventEmitter {
|
|
|
346
346
|
notFound: errors,
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
this.emit("
|
|
349
|
+
this.emit("cl:DeleteObjects", report, keys);
|
|
350
350
|
return report;
|
|
351
351
|
}
|
|
352
352
|
|
|
@@ -382,7 +382,7 @@ export class S3Client extends EventEmitter {
|
|
|
382
382
|
const deletedCount = deleteResponse.Deleted ? deleteResponse.Deleted.length : 0;
|
|
383
383
|
totalDeleted += deletedCount;
|
|
384
384
|
|
|
385
|
-
this.emit("
|
|
385
|
+
this.emit("cl:DeleteAll", {
|
|
386
386
|
prefix,
|
|
387
387
|
batch: deletedCount,
|
|
388
388
|
total: totalDeleted
|
|
@@ -392,7 +392,7 @@ export class S3Client extends EventEmitter {
|
|
|
392
392
|
continuationToken = listResponse.IsTruncated ? listResponse.NextContinuationToken : undefined;
|
|
393
393
|
} while (continuationToken);
|
|
394
394
|
|
|
395
|
-
this.emit("
|
|
395
|
+
this.emit("cl:DeleteAllComplete", {
|
|
396
396
|
prefix,
|
|
397
397
|
totalDeleted
|
|
398
398
|
});
|
|
@@ -428,7 +428,7 @@ export class S3Client extends EventEmitter {
|
|
|
428
428
|
if (!ok) {
|
|
429
429
|
throw new UnknownError("Unknown error in listObjects", { prefix, bucket: this.config.bucket, original: err });
|
|
430
430
|
}
|
|
431
|
-
this.emit("
|
|
431
|
+
this.emit("cl:ListObjects", response, options);
|
|
432
432
|
return response;
|
|
433
433
|
}
|
|
434
434
|
|
|
@@ -446,7 +446,7 @@ export class S3Client extends EventEmitter {
|
|
|
446
446
|
truncated = response.IsTruncated || false;
|
|
447
447
|
continuationToken = response.NextContinuationToken;
|
|
448
448
|
}
|
|
449
|
-
this.emit("
|
|
449
|
+
this.emit("cl:Count", count, { prefix });
|
|
450
450
|
return count;
|
|
451
451
|
}
|
|
452
452
|
|
|
@@ -471,7 +471,7 @@ export class S3Client extends EventEmitter {
|
|
|
471
471
|
.map((x) => x.replace(this.config.keyPrefix, ""))
|
|
472
472
|
.map((x) => (x.startsWith("/") ? x.replace(`/`, "") : x));
|
|
473
473
|
}
|
|
474
|
-
this.emit("
|
|
474
|
+
this.emit("cl:GetAllKeys", keys, { prefix });
|
|
475
475
|
return keys;
|
|
476
476
|
}
|
|
477
477
|
|
|
@@ -506,7 +506,7 @@ export class S3Client extends EventEmitter {
|
|
|
506
506
|
break;
|
|
507
507
|
}
|
|
508
508
|
}
|
|
509
|
-
this.emit("
|
|
509
|
+
this.emit("cl:GetContinuationTokenAfterOffset", continuationToken || null, params);
|
|
510
510
|
return continuationToken || null;
|
|
511
511
|
}
|
|
512
512
|
|
|
@@ -525,7 +525,7 @@ export class S3Client extends EventEmitter {
|
|
|
525
525
|
offset,
|
|
526
526
|
});
|
|
527
527
|
if (!continuationToken) {
|
|
528
|
-
this.emit("
|
|
528
|
+
this.emit("cl:GetKeysPage", [], params);
|
|
529
529
|
return [];
|
|
530
530
|
}
|
|
531
531
|
}
|
|
@@ -550,7 +550,7 @@ export class S3Client extends EventEmitter {
|
|
|
550
550
|
.map((x) => x.replace(this.config.keyPrefix, ""))
|
|
551
551
|
.map((x) => (x.startsWith("/") ? x.replace(`/`, "") : x));
|
|
552
552
|
}
|
|
553
|
-
this.emit("
|
|
553
|
+
this.emit("cl:GetKeysPage", keys, params);
|
|
554
554
|
return keys;
|
|
555
555
|
}
|
|
556
556
|
|
|
@@ -572,7 +572,7 @@ export class S3Client extends EventEmitter {
|
|
|
572
572
|
}
|
|
573
573
|
return to;
|
|
574
574
|
});
|
|
575
|
-
this.emit("
|
|
575
|
+
this.emit("cl:MoveAllObjects", { results, errors }, { prefixFrom, prefixTo });
|
|
576
576
|
if (errors.length > 0) {
|
|
577
577
|
throw new UnknownError("Some objects could not be moved", {
|
|
578
578
|
bucket: this.config.bucket,
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Classifier - Determines if errors should be retried
|
|
3
|
+
*
|
|
4
|
+
* Classifies errors into RETRIABLE or NON_RETRIABLE categories
|
|
5
|
+
* based on error codes, HTTP status codes, and error properties.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const classification = ErrorClassifier.classify(error);
|
|
9
|
+
* if (classification === 'RETRIABLE') {
|
|
10
|
+
* // Retry the operation
|
|
11
|
+
* }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const RETRIABLE = 'RETRIABLE';
|
|
15
|
+
const NON_RETRIABLE = 'NON_RETRIABLE';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Network and timeout error codes (retriable)
|
|
19
|
+
*/
|
|
20
|
+
const RETRIABLE_NETWORK_CODES = new Set([
|
|
21
|
+
'ECONNREFUSED',
|
|
22
|
+
'ETIMEDOUT',
|
|
23
|
+
'ECONNRESET',
|
|
24
|
+
'EPIPE',
|
|
25
|
+
'ENOTFOUND',
|
|
26
|
+
'NetworkError',
|
|
27
|
+
'NETWORK_ERROR',
|
|
28
|
+
'TimeoutError',
|
|
29
|
+
'TIMEOUT'
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* AWS throttling and rate limit codes (retriable)
|
|
34
|
+
*/
|
|
35
|
+
const RETRIABLE_AWS_CODES = new Set([
|
|
36
|
+
'ThrottlingException',
|
|
37
|
+
'TooManyRequestsException',
|
|
38
|
+
'RequestLimitExceeded',
|
|
39
|
+
'ProvisionedThroughputExceededException',
|
|
40
|
+
'RequestThrottledException',
|
|
41
|
+
'SlowDown',
|
|
42
|
+
'ServiceUnavailable'
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* AWS conflict and conditional check failures (retriable)
|
|
47
|
+
*/
|
|
48
|
+
const RETRIABLE_AWS_CONFLICTS = new Set([
|
|
49
|
+
'ConditionalCheckFailedException',
|
|
50
|
+
'TransactionConflictException'
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Retriable HTTP status codes (5xx server errors, 429 rate limit)
|
|
55
|
+
*/
|
|
56
|
+
const RETRIABLE_STATUS_CODES = new Set([
|
|
57
|
+
429, // Too Many Requests
|
|
58
|
+
500, // Internal Server Error
|
|
59
|
+
502, // Bad Gateway
|
|
60
|
+
503, // Service Unavailable
|
|
61
|
+
504, // Gateway Timeout
|
|
62
|
+
507, // Insufficient Storage
|
|
63
|
+
509 // Bandwidth Limit Exceeded
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Non-retriable error names (validation, business logic)
|
|
68
|
+
*/
|
|
69
|
+
const NON_RETRIABLE_ERROR_NAMES = new Set([
|
|
70
|
+
'ValidationError',
|
|
71
|
+
'StateMachineError',
|
|
72
|
+
'SchemaError',
|
|
73
|
+
'AuthenticationError',
|
|
74
|
+
'PermissionError',
|
|
75
|
+
'BusinessLogicError',
|
|
76
|
+
'InvalidStateTransition'
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Non-retriable HTTP status codes (client errors)
|
|
81
|
+
*/
|
|
82
|
+
const NON_RETRIABLE_STATUS_CODES = new Set([
|
|
83
|
+
400, // Bad Request
|
|
84
|
+
401, // Unauthorized
|
|
85
|
+
403, // Forbidden
|
|
86
|
+
404, // Not Found
|
|
87
|
+
405, // Method Not Allowed
|
|
88
|
+
406, // Not Acceptable
|
|
89
|
+
409, // Conflict
|
|
90
|
+
410, // Gone
|
|
91
|
+
422 // Unprocessable Entity
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
export class ErrorClassifier {
|
|
95
|
+
/**
|
|
96
|
+
* Classify an error as RETRIABLE or NON_RETRIABLE
|
|
97
|
+
*
|
|
98
|
+
* @param {Error} error - The error to classify
|
|
99
|
+
* @param {Object} options - Classification options
|
|
100
|
+
* @param {Array<string>} options.retryableErrors - Custom retriable error names/codes
|
|
101
|
+
* @param {Array<string>} options.nonRetriableErrors - Custom non-retriable error names/codes
|
|
102
|
+
* @returns {string} 'RETRIABLE' or 'NON_RETRIABLE'
|
|
103
|
+
*/
|
|
104
|
+
static classify(error, options = {}) {
|
|
105
|
+
if (!error) return NON_RETRIABLE;
|
|
106
|
+
|
|
107
|
+
const {
|
|
108
|
+
retryableErrors = [],
|
|
109
|
+
nonRetriableErrors = []
|
|
110
|
+
} = options;
|
|
111
|
+
|
|
112
|
+
// Check custom error lists first
|
|
113
|
+
if (retryableErrors.length > 0) {
|
|
114
|
+
const isCustomRetriable = retryableErrors.some(errType =>
|
|
115
|
+
error.code === errType ||
|
|
116
|
+
error.name === errType ||
|
|
117
|
+
error.message?.includes(errType)
|
|
118
|
+
);
|
|
119
|
+
if (isCustomRetriable) return RETRIABLE;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (nonRetriableErrors.length > 0) {
|
|
123
|
+
const isCustomNonRetriable = nonRetriableErrors.some(errType =>
|
|
124
|
+
error.code === errType ||
|
|
125
|
+
error.name === errType ||
|
|
126
|
+
error.message?.includes(errType)
|
|
127
|
+
);
|
|
128
|
+
if (isCustomNonRetriable) return NON_RETRIABLE;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check explicit retriable property on error
|
|
132
|
+
if (error.retriable === false) return NON_RETRIABLE;
|
|
133
|
+
if (error.retriable === true) return RETRIABLE;
|
|
134
|
+
|
|
135
|
+
// Check for non-retriable error names
|
|
136
|
+
if (NON_RETRIABLE_ERROR_NAMES.has(error.name)) {
|
|
137
|
+
return NON_RETRIABLE;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check for non-retriable HTTP status codes
|
|
141
|
+
if (error.statusCode && NON_RETRIABLE_STATUS_CODES.has(error.statusCode)) {
|
|
142
|
+
return NON_RETRIABLE;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check for retriable network errors
|
|
146
|
+
if (error.code && RETRIABLE_NETWORK_CODES.has(error.code)) {
|
|
147
|
+
return RETRIABLE;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check for retriable AWS errors
|
|
151
|
+
if (error.code && RETRIABLE_AWS_CODES.has(error.code)) {
|
|
152
|
+
return RETRIABLE;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check for retriable AWS conflicts
|
|
156
|
+
if (error.code && RETRIABLE_AWS_CONFLICTS.has(error.code)) {
|
|
157
|
+
return RETRIABLE;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check for retriable HTTP status codes
|
|
161
|
+
if (error.statusCode && RETRIABLE_STATUS_CODES.has(error.statusCode)) {
|
|
162
|
+
return RETRIABLE;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check for timeout in error message
|
|
166
|
+
if (error.message && typeof error.message === 'string') {
|
|
167
|
+
const lowerMessage = error.message.toLowerCase();
|
|
168
|
+
if (lowerMessage.includes('timeout') ||
|
|
169
|
+
lowerMessage.includes('timed out') ||
|
|
170
|
+
lowerMessage.includes('network') ||
|
|
171
|
+
lowerMessage.includes('connection')) {
|
|
172
|
+
return RETRIABLE;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Default: treat as retriable (conservative approach)
|
|
177
|
+
// This ensures transient failures are retried by default
|
|
178
|
+
return RETRIABLE;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check if an error is retriable
|
|
183
|
+
*
|
|
184
|
+
* @param {Error} error - The error to check
|
|
185
|
+
* @param {Object} options - Classification options
|
|
186
|
+
* @returns {boolean} true if retriable
|
|
187
|
+
*/
|
|
188
|
+
static isRetriable(error, options = {}) {
|
|
189
|
+
return this.classify(error, options) === RETRIABLE;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check if an error is non-retriable
|
|
194
|
+
*
|
|
195
|
+
* @param {Error} error - The error to check
|
|
196
|
+
* @param {Object} options - Classification options
|
|
197
|
+
* @returns {boolean} true if non-retriable
|
|
198
|
+
*/
|
|
199
|
+
static isNonRetriable(error, options = {}) {
|
|
200
|
+
return this.classify(error, options) === NON_RETRIABLE;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { RETRIABLE, NON_RETRIABLE };
|
package/src/database.class.js
CHANGED
|
@@ -255,13 +255,13 @@ export class Database extends EventEmitter {
|
|
|
255
255
|
|
|
256
256
|
// Emit definition changes if any were detected
|
|
257
257
|
if (definitionChanges.length > 0) {
|
|
258
|
-
this.emit("
|
|
258
|
+
this.emit("db:resource-definitions-changed", {
|
|
259
259
|
changes: definitionChanges,
|
|
260
260
|
metadata: this.savedMetadata
|
|
261
261
|
});
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
this.emit("connected", new Date());
|
|
264
|
+
this.emit("db:connected", new Date());
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
/**
|
|
@@ -527,7 +527,7 @@ export class Database extends EventEmitter {
|
|
|
527
527
|
this.pluginList.splice(index, 1);
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
this.emit('plugin
|
|
530
|
+
this.emit('db:plugin:uninstalled', { name: pluginName, plugin });
|
|
531
531
|
}
|
|
532
532
|
|
|
533
533
|
async uploadMetadataFile() {
|
|
@@ -598,7 +598,7 @@ export class Database extends EventEmitter {
|
|
|
598
598
|
});
|
|
599
599
|
|
|
600
600
|
this.savedMetadata = metadata;
|
|
601
|
-
this.emit('
|
|
601
|
+
this.emit('db:metadata-uploaded', metadata);
|
|
602
602
|
}
|
|
603
603
|
|
|
604
604
|
blankMetadataStructure() {
|
|
@@ -918,7 +918,7 @@ export class Database extends EventEmitter {
|
|
|
918
918
|
contentType: 'application/json'
|
|
919
919
|
});
|
|
920
920
|
|
|
921
|
-
this.emit('
|
|
921
|
+
this.emit('db:metadata-healed', { healingLog, metadata });
|
|
922
922
|
|
|
923
923
|
if (this.verbose) {
|
|
924
924
|
console.warn('S3DB: Successfully uploaded healed metadata');
|
|
@@ -1091,7 +1091,7 @@ export class Database extends EventEmitter {
|
|
|
1091
1091
|
if (!existingVersionData || existingVersionData.hash !== newHash) {
|
|
1092
1092
|
await this.uploadMetadataFile();
|
|
1093
1093
|
}
|
|
1094
|
-
this.emit("
|
|
1094
|
+
this.emit("db:resource:updated", name);
|
|
1095
1095
|
return existingResource;
|
|
1096
1096
|
}
|
|
1097
1097
|
const existingMetadata = this.savedMetadata?.resources?.[name];
|
|
@@ -1131,7 +1131,7 @@ export class Database extends EventEmitter {
|
|
|
1131
1131
|
}
|
|
1132
1132
|
|
|
1133
1133
|
await this.uploadMetadataFile();
|
|
1134
|
-
this.emit("
|
|
1134
|
+
this.emit("db:resource:created", name);
|
|
1135
1135
|
return resource;
|
|
1136
1136
|
}
|
|
1137
1137
|
|
|
@@ -1290,7 +1290,7 @@ export class Database extends EventEmitter {
|
|
|
1290
1290
|
|
|
1291
1291
|
// 4. Emit disconnected event BEFORE removing database listeners (race condition fix)
|
|
1292
1292
|
// This ensures listeners can actually receive the event
|
|
1293
|
-
await this.emit('disconnected', new Date());
|
|
1293
|
+
await this.emit('db:disconnected', new Date());
|
|
1294
1294
|
|
|
1295
1295
|
// 5. Remove all listeners from the database itself
|
|
1296
1296
|
this.removeAllListeners();
|
|
@@ -1427,7 +1427,7 @@ export class Database extends EventEmitter {
|
|
|
1427
1427
|
const [ok, error] = await tryFn(() => hook({ database: this, ...context }));
|
|
1428
1428
|
if (!ok) {
|
|
1429
1429
|
// Emit error event
|
|
1430
|
-
this.emit('
|
|
1430
|
+
this.emit('db:hook-error', { event, error, context });
|
|
1431
1431
|
|
|
1432
1432
|
// In strict mode, throw on first error instead of continuing
|
|
1433
1433
|
if (this.strictHooks) {
|
|
@@ -149,7 +149,7 @@ export class BackupPlugin extends Plugin {
|
|
|
149
149
|
console.log(`[BackupPlugin] Initialized with driver: ${storageInfo.type}`);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
this.emit('initialized', {
|
|
152
|
+
this.emit('db:plugin:initialized', {
|
|
153
153
|
driver: this.driver.getType(),
|
|
154
154
|
config: this.driver.getStorageInfo()
|
|
155
155
|
});
|
|
@@ -205,7 +205,7 @@ export class BackupPlugin extends Plugin {
|
|
|
205
205
|
await this._executeHook(this.config.onBackupStart, type, { backupId });
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
this.emit('
|
|
208
|
+
this.emit('plg:backup:start', { id: backupId, type });
|
|
209
209
|
|
|
210
210
|
// Create backup metadata
|
|
211
211
|
const metadata = await this._createBackupMetadata(backupId, type);
|
|
@@ -262,7 +262,7 @@ export class BackupPlugin extends Plugin {
|
|
|
262
262
|
await this._executeHook(this.config.onBackupComplete, type, stats);
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
this.emit('
|
|
265
|
+
this.emit('plg:backup:complete', {
|
|
266
266
|
id: backupId,
|
|
267
267
|
type,
|
|
268
268
|
size: totalSize,
|
|
@@ -300,7 +300,7 @@ export class BackupPlugin extends Plugin {
|
|
|
300
300
|
duration: Date.now() - startTime
|
|
301
301
|
});
|
|
302
302
|
|
|
303
|
-
this.emit('
|
|
303
|
+
this.emit('plg:backup:error', { id: backupId, type, error: error.message });
|
|
304
304
|
throw error;
|
|
305
305
|
|
|
306
306
|
} finally {
|
|
@@ -579,7 +579,7 @@ export class BackupPlugin extends Plugin {
|
|
|
579
579
|
await this._executeHook(this.config.onRestoreStart, backupId, options);
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
-
this.emit('
|
|
582
|
+
this.emit('plg:backup:restore-start', { id: backupId, options });
|
|
583
583
|
|
|
584
584
|
// Get backup metadata
|
|
585
585
|
const backup = await this.getBackupStatus(backupId);
|
|
@@ -616,7 +616,7 @@ export class BackupPlugin extends Plugin {
|
|
|
616
616
|
await this._executeHook(this.config.onRestoreComplete, backupId, { restored: restoredResources });
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
-
this.emit('
|
|
619
|
+
this.emit('plg:backup:restore-complete', {
|
|
620
620
|
id: backupId,
|
|
621
621
|
restored: restoredResources
|
|
622
622
|
});
|
|
@@ -637,7 +637,7 @@ export class BackupPlugin extends Plugin {
|
|
|
637
637
|
await this._executeHook(this.config.onRestoreError, backupId, { error });
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
-
this.emit('
|
|
640
|
+
this.emit('plg:backup:restore-error', { id: backupId, error: error.message });
|
|
641
641
|
throw error;
|
|
642
642
|
}
|
|
643
643
|
}
|
|
@@ -973,7 +973,7 @@ export class BackupPlugin extends Plugin {
|
|
|
973
973
|
async stop() {
|
|
974
974
|
// Cancel any active backups
|
|
975
975
|
for (const backupId of this.activeBackups) {
|
|
976
|
-
this.emit('
|
|
976
|
+
this.emit('plg:backup:cancelled', { id: backupId });
|
|
977
977
|
}
|
|
978
978
|
this.activeBackups.clear();
|
|
979
979
|
|
|
@@ -434,7 +434,7 @@ export class CachePlugin extends Plugin {
|
|
|
434
434
|
const [ok, err] = await this.clearCacheWithRetry(resource.cache, specificKey);
|
|
435
435
|
|
|
436
436
|
if (!ok) {
|
|
437
|
-
this.emit('
|
|
437
|
+
this.emit('plg:cache:clear-error', {
|
|
438
438
|
resource: resource.name,
|
|
439
439
|
method,
|
|
440
440
|
id: data.id,
|
|
@@ -456,7 +456,7 @@ export class CachePlugin extends Plugin {
|
|
|
456
456
|
const [ok, err] = await this.clearCacheWithRetry(resource.cache, partitionKeyPrefix);
|
|
457
457
|
|
|
458
458
|
if (!ok) {
|
|
459
|
-
this.emit('
|
|
459
|
+
this.emit('plg:cache:clear-error', {
|
|
460
460
|
resource: resource.name,
|
|
461
461
|
partition: partitionName,
|
|
462
462
|
error: err.message
|
|
@@ -475,7 +475,7 @@ export class CachePlugin extends Plugin {
|
|
|
475
475
|
const [ok, err] = await this.clearCacheWithRetry(resource.cache, keyPrefix);
|
|
476
476
|
|
|
477
477
|
if (!ok) {
|
|
478
|
-
this.emit('
|
|
478
|
+
this.emit('plg:cache:clear-error', {
|
|
479
479
|
resource: resource.name,
|
|
480
480
|
type: 'broad',
|
|
481
481
|
error: err.message
|