s3db.js 13.6.0 → 14.0.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 +139 -43
- package/dist/s3db.cjs +72425 -38970
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72177 -38764
- package/dist/s3db.es.js.map +1 -1
- package/mcp/lib/base-handler.js +157 -0
- package/mcp/lib/handlers/connection-handler.js +280 -0
- package/mcp/lib/handlers/query-handler.js +533 -0
- package/mcp/lib/handlers/resource-handler.js +428 -0
- package/mcp/lib/tool-registry.js +336 -0
- package/mcp/lib/tools/connection-tools.js +161 -0
- package/mcp/lib/tools/query-tools.js +267 -0
- package/mcp/lib/tools/resource-tools.js +404 -0
- package/package.json +94 -49
- package/src/clients/memory-client.class.js +346 -191
- package/src/clients/memory-storage.class.js +300 -84
- package/src/clients/s3-client.class.js +7 -6
- package/src/concerns/geo-encoding.js +19 -2
- package/src/concerns/ip.js +59 -9
- package/src/concerns/money.js +8 -1
- package/src/concerns/password-hashing.js +49 -8
- package/src/concerns/plugin-storage.js +186 -18
- package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
- package/src/database.class.js +139 -29
- package/src/errors.js +332 -42
- package/src/plugins/api/auth/oidc-auth.js +66 -17
- package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
- package/src/plugins/api/auth/strategies/factory.class.js +63 -0
- package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
- package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
- package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
- package/src/plugins/api/concerns/failban-manager.js +106 -57
- package/src/plugins/api/concerns/opengraph-helper.js +116 -0
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/concerns/state-machine.js +288 -0
- package/src/plugins/api/index.js +180 -41
- package/src/plugins/api/routes/auth-routes.js +198 -30
- package/src/plugins/api/routes/resource-routes.js +19 -4
- package/src/plugins/api/server/health-manager.class.js +163 -0
- package/src/plugins/api/server/middleware-chain.class.js +310 -0
- package/src/plugins/api/server/router.class.js +472 -0
- package/src/plugins/api/server.js +280 -1303
- package/src/plugins/api/utils/custom-routes.js +17 -5
- package/src/plugins/api/utils/guards.js +76 -17
- package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
- package/src/plugins/api/utils/openapi-generator.js +7 -6
- package/src/plugins/api/utils/template-engine.js +77 -3
- package/src/plugins/audit.plugin.js +30 -8
- package/src/plugins/backup.plugin.js +110 -14
- package/src/plugins/cache/cache.class.js +22 -5
- package/src/plugins/cache/filesystem-cache.class.js +116 -19
- package/src/plugins/cache/memory-cache.class.js +211 -57
- package/src/plugins/cache/multi-tier-cache.class.js +371 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
- package/src/plugins/cache/redis-cache.class.js +552 -0
- package/src/plugins/cache/s3-cache.class.js +17 -8
- package/src/plugins/cache.plugin.js +176 -61
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
- package/src/plugins/cloud-inventory/index.js +29 -8
- package/src/plugins/cloud-inventory/registry.js +64 -42
- package/src/plugins/cloud-inventory.plugin.js +240 -138
- package/src/plugins/concerns/plugin-dependencies.js +54 -0
- package/src/plugins/concerns/resource-names.js +100 -0
- package/src/plugins/consumers/index.js +10 -2
- package/src/plugins/consumers/sqs-consumer.js +12 -2
- package/src/plugins/cookie-farm-suite.plugin.js +278 -0
- package/src/plugins/cookie-farm.errors.js +73 -0
- package/src/plugins/cookie-farm.plugin.js +869 -0
- package/src/plugins/costs.plugin.js +7 -1
- package/src/plugins/eventual-consistency/analytics.js +94 -19
- package/src/plugins/eventual-consistency/config.js +15 -7
- package/src/plugins/eventual-consistency/consolidation.js +29 -11
- package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
- package/src/plugins/eventual-consistency/helpers.js +39 -14
- package/src/plugins/eventual-consistency/install.js +21 -2
- package/src/plugins/eventual-consistency/utils.js +32 -10
- package/src/plugins/fulltext.plugin.js +38 -11
- package/src/plugins/geo.plugin.js +61 -9
- package/src/plugins/identity/concerns/config.js +61 -0
- package/src/plugins/identity/concerns/mfa-manager.js +15 -2
- package/src/plugins/identity/concerns/rate-limit.js +124 -0
- package/src/plugins/identity/concerns/resource-schemas.js +9 -1
- package/src/plugins/identity/concerns/token-generator.js +29 -4
- package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
- package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
- package/src/plugins/identity/drivers/index.js +18 -0
- package/src/plugins/identity/drivers/password-driver.js +122 -0
- package/src/plugins/identity/email-service.js +17 -2
- package/src/plugins/identity/index.js +413 -69
- package/src/plugins/identity/oauth2-server.js +413 -30
- package/src/plugins/identity/oidc-discovery.js +16 -8
- package/src/plugins/identity/rsa-keys.js +115 -35
- package/src/plugins/identity/server.js +166 -45
- package/src/plugins/identity/session-manager.js +53 -7
- package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
- package/src/plugins/identity/ui/routes.js +363 -255
- package/src/plugins/importer/index.js +153 -20
- package/src/plugins/index.js +9 -2
- package/src/plugins/kubernetes-inventory/index.js +6 -0
- package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
- package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
- package/src/plugins/kubernetes-inventory.plugin.js +980 -0
- package/src/plugins/metrics.plugin.js +64 -16
- package/src/plugins/ml/base-model.class.js +25 -15
- package/src/plugins/ml/regression-model.class.js +1 -1
- package/src/plugins/ml.errors.js +57 -25
- package/src/plugins/ml.plugin.js +28 -4
- package/src/plugins/namespace.js +210 -0
- package/src/plugins/plugin.class.js +180 -8
- package/src/plugins/puppeteer/console-monitor.js +729 -0
- package/src/plugins/puppeteer/cookie-manager.js +492 -0
- package/src/plugins/puppeteer/network-monitor.js +816 -0
- package/src/plugins/puppeteer/performance-manager.js +746 -0
- package/src/plugins/puppeteer/proxy-manager.js +478 -0
- package/src/plugins/puppeteer/stealth-manager.js +556 -0
- package/src/plugins/puppeteer.errors.js +81 -0
- package/src/plugins/puppeteer.plugin.js +1327 -0
- package/src/plugins/queue-consumer.plugin.js +69 -14
- package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
- package/src/plugins/recon/concerns/command-runner.js +148 -0
- package/src/plugins/recon/concerns/diff-detector.js +372 -0
- package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
- package/src/plugins/recon/concerns/process-manager.js +338 -0
- package/src/plugins/recon/concerns/report-generator.js +478 -0
- package/src/plugins/recon/concerns/security-analyzer.js +571 -0
- package/src/plugins/recon/concerns/target-normalizer.js +68 -0
- package/src/plugins/recon/config/defaults.js +321 -0
- package/src/plugins/recon/config/resources.js +370 -0
- package/src/plugins/recon/index.js +778 -0
- package/src/plugins/recon/managers/dependency-manager.js +174 -0
- package/src/plugins/recon/managers/scheduler-manager.js +179 -0
- package/src/plugins/recon/managers/storage-manager.js +745 -0
- package/src/plugins/recon/managers/target-manager.js +274 -0
- package/src/plugins/recon/stages/asn-stage.js +314 -0
- package/src/plugins/recon/stages/certificate-stage.js +84 -0
- package/src/plugins/recon/stages/dns-stage.js +107 -0
- package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
- package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
- package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
- package/src/plugins/recon/stages/http-stage.js +89 -0
- package/src/plugins/recon/stages/latency-stage.js +148 -0
- package/src/plugins/recon/stages/massdns-stage.js +302 -0
- package/src/plugins/recon/stages/osint-stage.js +1373 -0
- package/src/plugins/recon/stages/ports-stage.js +169 -0
- package/src/plugins/recon/stages/screenshot-stage.js +94 -0
- package/src/plugins/recon/stages/secrets-stage.js +514 -0
- package/src/plugins/recon/stages/subdomains-stage.js +295 -0
- package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
- package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
- package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
- package/src/plugins/recon/stages/whois-stage.js +349 -0
- package/src/plugins/recon.plugin.js +75 -0
- package/src/plugins/recon.plugin.js.backup +2635 -0
- package/src/plugins/relation.errors.js +87 -14
- package/src/plugins/replicator.plugin.js +514 -137
- package/src/plugins/replicators/base-replicator.class.js +89 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
- package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mysql-replicator.class.js +52 -17
- package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
- package/src/plugins/replicators/postgres-replicator.class.js +62 -27
- package/src/plugins/replicators/s3db-replicator.class.js +25 -18
- package/src/plugins/replicators/schema-sync.helper.js +3 -3
- package/src/plugins/replicators/sqs-replicator.class.js +8 -2
- package/src/plugins/replicators/turso-replicator.class.js +23 -3
- package/src/plugins/replicators/webhook-replicator.class.js +42 -4
- package/src/plugins/s3-queue.plugin.js +464 -65
- package/src/plugins/scheduler.plugin.js +20 -6
- package/src/plugins/state-machine.plugin.js +40 -9
- package/src/plugins/tfstate/README.md +126 -126
- package/src/plugins/tfstate/base-driver.js +28 -4
- package/src/plugins/tfstate/errors.js +65 -10
- package/src/plugins/tfstate/filesystem-driver.js +52 -8
- package/src/plugins/tfstate/index.js +163 -90
- package/src/plugins/tfstate/s3-driver.js +64 -6
- package/src/plugins/ttl.plugin.js +72 -17
- package/src/plugins/vector/distances.js +18 -12
- package/src/plugins/vector/kmeans.js +26 -4
- package/src/resource.class.js +115 -19
- package/src/testing/factory.class.js +20 -3
- package/src/testing/seeder.class.js +7 -1
- package/src/clients/memory-client.md +0 -917
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Plugin } from "./plugin.class.js";
|
|
2
2
|
import tryFn from "../concerns/try-fn.js";
|
|
3
3
|
import { idGenerator } from "../concerns/id.js";
|
|
4
|
+
import { resolveResourceName } from "./concerns/resource-names.js";
|
|
5
|
+
import { QueueError } from "./queue.errors.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* S3QueuePlugin - Distributed Queue System with ETag-based Atomicity
|
|
@@ -64,24 +66,58 @@ export class S3QueuePlugin extends Plugin {
|
|
|
64
66
|
constructor(options = {}) {
|
|
65
67
|
super(options);
|
|
66
68
|
|
|
69
|
+
const resourceNamesOption = options.resourceNames || {};
|
|
67
70
|
if (!options.resource) {
|
|
68
|
-
throw new
|
|
71
|
+
throw new QueueError('S3QueuePlugin requires "resource" option', {
|
|
72
|
+
pluginName: 'S3QueuePlugin',
|
|
73
|
+
operation: 'constructor',
|
|
74
|
+
statusCode: 400,
|
|
75
|
+
retriable: false,
|
|
76
|
+
suggestion: 'Provide the target resource name: new S3QueuePlugin({ resource: "orders", ... }).'
|
|
77
|
+
});
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
this.config = {
|
|
81
|
+
...options,
|
|
72
82
|
resource: options.resource,
|
|
73
|
-
visibilityTimeout: options.visibilityTimeout
|
|
74
|
-
pollInterval: options.pollInterval
|
|
75
|
-
maxAttempts: options.maxAttempts
|
|
76
|
-
concurrency: options.concurrency
|
|
77
|
-
deadLetterResource: options.deadLetterResource
|
|
83
|
+
visibilityTimeout: options.visibilityTimeout ?? 30000, // 30 seconds
|
|
84
|
+
pollInterval: options.pollInterval ?? 1000, // 1 second
|
|
85
|
+
maxAttempts: options.maxAttempts ?? 3,
|
|
86
|
+
concurrency: options.concurrency ?? 1,
|
|
87
|
+
deadLetterResource: options.deadLetterResource ?? null,
|
|
78
88
|
autoStart: options.autoStart !== false,
|
|
79
89
|
onMessage: options.onMessage,
|
|
80
90
|
onError: options.onError,
|
|
81
91
|
onComplete: options.onComplete,
|
|
82
|
-
verbose: options.verbose
|
|
83
|
-
|
|
92
|
+
verbose: options.verbose ?? false
|
|
93
|
+
};
|
|
94
|
+
this.config.pollBatchSize = options.pollBatchSize ?? Math.max((this.config.concurrency || 1) * 4, 16);
|
|
95
|
+
this.config.recoveryInterval = options.recoveryInterval ?? 5000;
|
|
96
|
+
this.config.recoveryBatchSize = options.recoveryBatchSize ?? Math.max((this.config.concurrency || 1) * 2, 10);
|
|
97
|
+
this.config.processedCacheTTL = options.processedCacheTTL ?? 30000;
|
|
98
|
+
this.config.maxPollInterval = options.maxPollInterval ?? this.config.pollInterval;
|
|
99
|
+
|
|
100
|
+
this._queueResourceDescriptor = {
|
|
101
|
+
defaultName: `plg_s3queue_${this.config.resource}_queue`,
|
|
102
|
+
override: resourceNamesOption.queue || options.queueResource
|
|
84
103
|
};
|
|
104
|
+
this.queueResourceName = this._resolveQueueResourceName();
|
|
105
|
+
this.config.queueResourceName = this.queueResourceName;
|
|
106
|
+
|
|
107
|
+
if (this.config.deadLetterResource) {
|
|
108
|
+
this._deadLetterDescriptor = {
|
|
109
|
+
defaultName: `plg_s3queue_${this.config.resource}_dead`,
|
|
110
|
+
override: resourceNamesOption.deadLetter || this.config.deadLetterResource
|
|
111
|
+
};
|
|
112
|
+
} else {
|
|
113
|
+
this._deadLetterDescriptor = null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.deadLetterResourceName = this._resolveDeadLetterResourceName();
|
|
117
|
+
this.config.deadLetterResource = this.deadLetterResourceName;
|
|
118
|
+
|
|
119
|
+
this.queueResourceAlias = options.queueResource || `${this.config.resource}_queue`;
|
|
120
|
+
this.deadLetterResourceAlias = options.deadLetterResource || null;
|
|
85
121
|
|
|
86
122
|
this.queueResource = null; // Resource: <resource>_queue
|
|
87
123
|
this.targetResource = null; // Resource original do usuário
|
|
@@ -92,20 +128,63 @@ export class S3QueuePlugin extends Plugin {
|
|
|
92
128
|
|
|
93
129
|
// Deduplication cache to prevent S3 eventual consistency issues
|
|
94
130
|
// Tracks recently processed messages to avoid reprocessing
|
|
95
|
-
this.processedCache = new Map(); // queueId ->
|
|
131
|
+
this.processedCache = new Map(); // queueId -> expiresAt
|
|
96
132
|
this.cacheCleanupInterval = null;
|
|
97
133
|
this.lockCleanupInterval = null;
|
|
134
|
+
this.messageLocks = new Map();
|
|
135
|
+
this._lastRecovery = 0;
|
|
136
|
+
this._recoveryInFlight = false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_resolveQueueResourceName() {
|
|
140
|
+
return resolveResourceName('s3queue', this._queueResourceDescriptor, {
|
|
141
|
+
namespace: this.namespace
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_resolveDeadLetterResourceName() {
|
|
146
|
+
if (!this._deadLetterDescriptor) return null;
|
|
147
|
+
const { override, defaultName } = this._deadLetterDescriptor;
|
|
148
|
+
if (override) {
|
|
149
|
+
// Honor explicit overrides verbatim unless user opted into plg_* naming.
|
|
150
|
+
if (override.startsWith('plg_')) {
|
|
151
|
+
return resolveResourceName('s3queue', { override }, {
|
|
152
|
+
namespace: this.namespace,
|
|
153
|
+
applyNamespaceToOverrides: true
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return override;
|
|
157
|
+
}
|
|
158
|
+
return resolveResourceName('s3queue', { defaultName }, {
|
|
159
|
+
namespace: this.namespace
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
onNamespaceChanged() {
|
|
164
|
+
if (!this._queueResourceDescriptor) return;
|
|
165
|
+
this.queueResourceName = this._resolveQueueResourceName();
|
|
166
|
+
this.config.queueResourceName = this.queueResourceName;
|
|
167
|
+
this.deadLetterResourceName = this._resolveDeadLetterResourceName();
|
|
168
|
+
this.config.deadLetterResource = this.deadLetterResourceName;
|
|
98
169
|
}
|
|
99
170
|
|
|
100
171
|
async onInstall() {
|
|
101
172
|
// Get target resource
|
|
102
173
|
this.targetResource = this.database.resources[this.config.resource];
|
|
103
174
|
if (!this.targetResource) {
|
|
104
|
-
throw new
|
|
175
|
+
throw new QueueError(`Resource '${this.config.resource}' not found`, {
|
|
176
|
+
pluginName: 'S3QueuePlugin',
|
|
177
|
+
operation: 'onInstall',
|
|
178
|
+
resourceName: this.config.resource,
|
|
179
|
+
statusCode: 404,
|
|
180
|
+
retriable: false,
|
|
181
|
+
suggestion: 'Create the resource before installing S3QueuePlugin or update the plugin configuration.',
|
|
182
|
+
availableResources: Object.keys(this.database.resources || {})
|
|
183
|
+
});
|
|
105
184
|
}
|
|
106
185
|
|
|
107
186
|
// Create queue metadata resource
|
|
108
|
-
const queueName =
|
|
187
|
+
const queueName = this.queueResourceName;
|
|
109
188
|
const [ok, err] = await tryFn(() =>
|
|
110
189
|
this.database.createResource({
|
|
111
190
|
name: queueName,
|
|
@@ -133,11 +212,30 @@ export class S3QueuePlugin extends Plugin {
|
|
|
133
212
|
})
|
|
134
213
|
);
|
|
135
214
|
|
|
136
|
-
if (
|
|
137
|
-
|
|
215
|
+
if (ok) {
|
|
216
|
+
this.queueResource = this.database.resources[queueName];
|
|
217
|
+
} else {
|
|
218
|
+
this.queueResource = this.database.resources[queueName];
|
|
219
|
+
if (!this.queueResource) {
|
|
220
|
+
throw new QueueError(`Failed to create queue resource: ${err?.message}`, {
|
|
221
|
+
pluginName: 'S3QueuePlugin',
|
|
222
|
+
operation: 'createQueueResource',
|
|
223
|
+
queueName,
|
|
224
|
+
statusCode: 500,
|
|
225
|
+
retriable: false,
|
|
226
|
+
suggestion: 'Check database permissions and ensure createResource() was successful.',
|
|
227
|
+
original: err
|
|
228
|
+
});
|
|
229
|
+
}
|
|
138
230
|
}
|
|
231
|
+
this.queueResourceName = this.queueResource.name;
|
|
139
232
|
|
|
140
|
-
this.
|
|
233
|
+
if (this.queueResourceAlias) {
|
|
234
|
+
const existing = this.database.resources[this.queueResourceAlias];
|
|
235
|
+
if (!existing || existing === this.queueResource) {
|
|
236
|
+
this.database.resources[this.queueResourceAlias] = this.queueResource;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
141
239
|
|
|
142
240
|
// Locks are now managed by PluginStorage with TTL - no Resource needed
|
|
143
241
|
// Lock acquisition is handled via storage.acquireLock() with automatic expiration
|
|
@@ -220,6 +318,17 @@ export class S3QueuePlugin extends Plugin {
|
|
|
220
318
|
resource.stopProcessing = async function() {
|
|
221
319
|
return await plugin.stopProcessing();
|
|
222
320
|
};
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Extend visibility timeout for a specific queue entry
|
|
324
|
+
*/
|
|
325
|
+
resource.extendQueueVisibility = async function(queueId, extraMilliseconds) {
|
|
326
|
+
return await plugin.extendVisibility(queueId, extraMilliseconds);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
resource.clearQueueCache = async function() {
|
|
330
|
+
plugin.clearProcessedCache();
|
|
331
|
+
};
|
|
223
332
|
}
|
|
224
333
|
|
|
225
334
|
async startProcessing(handler = null, options = {}) {
|
|
@@ -232,7 +341,14 @@ export class S3QueuePlugin extends Plugin {
|
|
|
232
341
|
|
|
233
342
|
const messageHandler = handler || this.config.onMessage;
|
|
234
343
|
if (!messageHandler) {
|
|
235
|
-
throw new
|
|
344
|
+
throw new QueueError('onMessage handler required', {
|
|
345
|
+
pluginName: 'S3QueuePlugin',
|
|
346
|
+
operation: 'startProcessing',
|
|
347
|
+
queueName: this.queueResourceName,
|
|
348
|
+
statusCode: 400,
|
|
349
|
+
retriable: false,
|
|
350
|
+
suggestion: 'Pass a handler: resource.startProcessing(async msg => {...}) or configure onMessage in plugin options.'
|
|
351
|
+
});
|
|
236
352
|
}
|
|
237
353
|
|
|
238
354
|
this.isRunning = true;
|
|
@@ -241,16 +357,17 @@ export class S3QueuePlugin extends Plugin {
|
|
|
241
357
|
// Start cache cleanup (every 5 seconds, remove entries older than 30 seconds)
|
|
242
358
|
this.cacheCleanupInterval = setInterval(() => {
|
|
243
359
|
const now = Date.now();
|
|
244
|
-
const
|
|
360
|
+
const ttl = this.config.processedCacheTTL;
|
|
245
361
|
|
|
246
|
-
for (const [queueId,
|
|
247
|
-
if (now -
|
|
362
|
+
for (const [queueId, expiresAt] of this.processedCache.entries()) {
|
|
363
|
+
if (expiresAt <= now || expiresAt - now > ttl * 4) {
|
|
248
364
|
this.processedCache.delete(queueId);
|
|
249
365
|
}
|
|
250
366
|
}
|
|
251
367
|
}, 5000);
|
|
252
368
|
|
|
253
369
|
// Lock cleanup no longer needed - TTL handles expiration automatically
|
|
370
|
+
this._lastRecovery = 0;
|
|
254
371
|
|
|
255
372
|
// Start N workers
|
|
256
373
|
for (let i = 0; i < concurrency; i++) {
|
|
@@ -294,24 +411,28 @@ export class S3QueuePlugin extends Plugin {
|
|
|
294
411
|
|
|
295
412
|
createWorker(handler, workerIndex) {
|
|
296
413
|
return (async () => {
|
|
414
|
+
let idleStreak = 0;
|
|
297
415
|
while (this.isRunning) {
|
|
298
416
|
try {
|
|
299
417
|
// Try to claim a message
|
|
300
418
|
const message = await this.claimMessage();
|
|
301
419
|
|
|
302
420
|
if (message) {
|
|
421
|
+
idleStreak = 0;
|
|
303
422
|
// Process the claimed message
|
|
304
423
|
await this.processMessage(message, handler);
|
|
305
424
|
} else {
|
|
306
425
|
// No messages available, wait before polling again
|
|
307
|
-
|
|
426
|
+
idleStreak = Math.min(idleStreak + 1, 10);
|
|
427
|
+
const delay = this._computeIdleDelay(idleStreak);
|
|
428
|
+
await this._sleep(delay);
|
|
308
429
|
}
|
|
309
430
|
} catch (error) {
|
|
310
431
|
if (this.config.verbose) {
|
|
311
432
|
console.error(`[Worker ${workerIndex}] Error:`, error.message);
|
|
312
433
|
}
|
|
313
434
|
// Wait a bit before retrying on error
|
|
314
|
-
await
|
|
435
|
+
await this._sleep(1000);
|
|
315
436
|
}
|
|
316
437
|
}
|
|
317
438
|
})();
|
|
@@ -320,10 +441,15 @@ export class S3QueuePlugin extends Plugin {
|
|
|
320
441
|
async claimMessage() {
|
|
321
442
|
const now = Date.now();
|
|
322
443
|
|
|
444
|
+
await this.recoverStalledMessages(now);
|
|
445
|
+
|
|
323
446
|
// Query for available messages
|
|
324
447
|
const [ok, err, messages] = await tryFn(() =>
|
|
325
448
|
this.queueResource.query({
|
|
326
|
-
status: 'pending'
|
|
449
|
+
status: 'pending',
|
|
450
|
+
visibleAt: { '<=': now }
|
|
451
|
+
}, {
|
|
452
|
+
limit: this.config.pollBatchSize
|
|
327
453
|
})
|
|
328
454
|
);
|
|
329
455
|
|
|
@@ -352,40 +478,63 @@ export class S3QueuePlugin extends Plugin {
|
|
|
352
478
|
* Acquire a distributed lock using PluginStorage TTL
|
|
353
479
|
* This ensures only one worker can claim a message at a time
|
|
354
480
|
*/
|
|
481
|
+
_lockNameForMessage(messageId) {
|
|
482
|
+
return `msg-${messageId}`;
|
|
483
|
+
}
|
|
484
|
+
|
|
355
485
|
async acquireLock(messageId) {
|
|
356
486
|
const storage = this.getStorage();
|
|
357
|
-
const
|
|
487
|
+
const lockName = this._lockNameForMessage(messageId);
|
|
358
488
|
|
|
359
489
|
try {
|
|
360
|
-
const lock = await storage.acquireLock(
|
|
490
|
+
const lock = await storage.acquireLock(lockName, {
|
|
361
491
|
ttl: 5, // 5 seconds
|
|
362
492
|
timeout: 0, // Don't wait if locked
|
|
363
493
|
workerId: this.workerId
|
|
364
494
|
});
|
|
365
495
|
|
|
366
|
-
|
|
496
|
+
if (lock) {
|
|
497
|
+
this.messageLocks.set(lock.name, lock);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return lock;
|
|
367
501
|
} catch (error) {
|
|
368
502
|
// On any error, skip this message
|
|
369
503
|
if (this.config.verbose) {
|
|
370
504
|
console.log(`[acquireLock] Error: ${error.message}`);
|
|
371
505
|
}
|
|
372
|
-
return
|
|
506
|
+
return null;
|
|
373
507
|
}
|
|
374
508
|
}
|
|
375
509
|
|
|
376
510
|
/**
|
|
377
511
|
* Release a distributed lock via PluginStorage
|
|
378
512
|
*/
|
|
379
|
-
async releaseLock(
|
|
513
|
+
async releaseLock(lockOrMessageId) {
|
|
380
514
|
const storage = this.getStorage();
|
|
381
|
-
|
|
515
|
+
let lock = null;
|
|
516
|
+
|
|
517
|
+
if (lockOrMessageId && typeof lockOrMessageId === 'object') {
|
|
518
|
+
lock = lockOrMessageId;
|
|
519
|
+
} else {
|
|
520
|
+
const lockName = this._lockNameForMessage(lockOrMessageId);
|
|
521
|
+
lock = this.messageLocks.get(lockName) || null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (!lock) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
382
527
|
|
|
383
528
|
try {
|
|
384
|
-
await storage.releaseLock(
|
|
529
|
+
await storage.releaseLock(lock);
|
|
385
530
|
} catch (error) {
|
|
386
531
|
// Ignore errors on release (lock may have expired or been cleaned up)
|
|
387
532
|
if (this.config.verbose) {
|
|
388
|
-
console.log(`[releaseLock] Failed to release lock
|
|
533
|
+
console.log(`[releaseLock] Failed to release lock '${lock.name}': ${error.message}`);
|
|
534
|
+
}
|
|
535
|
+
} finally {
|
|
536
|
+
if (lock?.name) {
|
|
537
|
+
this.messageLocks.delete(lock.name);
|
|
389
538
|
}
|
|
390
539
|
}
|
|
391
540
|
}
|
|
@@ -404,28 +553,29 @@ export class S3QueuePlugin extends Plugin {
|
|
|
404
553
|
|
|
405
554
|
// Try to acquire distributed lock for cache check
|
|
406
555
|
// This prevents race condition where multiple workers check cache simultaneously
|
|
407
|
-
const
|
|
556
|
+
const lock = await this.acquireLock(msg.id);
|
|
408
557
|
|
|
409
|
-
if (!
|
|
558
|
+
if (!lock) {
|
|
410
559
|
// Another worker is checking/claiming this message, skip it
|
|
411
560
|
return null;
|
|
412
561
|
}
|
|
413
562
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
await this.
|
|
417
|
-
if (
|
|
418
|
-
|
|
563
|
+
try {
|
|
564
|
+
// Check deduplication cache (protected by lock)
|
|
565
|
+
const alreadyProcessed = await this._isRecentlyProcessed(msg.id);
|
|
566
|
+
if (alreadyProcessed) {
|
|
567
|
+
if (this.config.verbose) {
|
|
568
|
+
console.log(`[attemptClaim] Message ${msg.id} already processed (in cache)`);
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
419
571
|
}
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
572
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
573
|
+
// Add to cache immediately (while still holding lock)
|
|
574
|
+
// This prevents other workers from claiming this message
|
|
575
|
+
await this._markMessageProcessed(msg.id);
|
|
576
|
+
} finally {
|
|
577
|
+
await this.releaseLock(lock);
|
|
578
|
+
}
|
|
429
579
|
|
|
430
580
|
// Fetch the message with ETag (query doesn't return _etag)
|
|
431
581
|
const [okGet, errGet, msgWithETag] = await tryFn(() =>
|
|
@@ -434,7 +584,7 @@ export class S3QueuePlugin extends Plugin {
|
|
|
434
584
|
|
|
435
585
|
if (!okGet || !msgWithETag) {
|
|
436
586
|
// Message was deleted or not found - remove from cache
|
|
437
|
-
this.
|
|
587
|
+
await this._clearProcessedMarker(msg.id);
|
|
438
588
|
if (this.config.verbose) {
|
|
439
589
|
console.log(`[attemptClaim] Message ${msg.id} not found or error: ${errGet?.message}`);
|
|
440
590
|
}
|
|
@@ -575,6 +725,7 @@ export class S3QueuePlugin extends Plugin {
|
|
|
575
725
|
status: 'failed',
|
|
576
726
|
error
|
|
577
727
|
});
|
|
728
|
+
await this._clearProcessedMarker(queueId);
|
|
578
729
|
}
|
|
579
730
|
|
|
580
731
|
async retryMessage(queueId, attempts, error) {
|
|
@@ -588,7 +739,7 @@ export class S3QueuePlugin extends Plugin {
|
|
|
588
739
|
});
|
|
589
740
|
|
|
590
741
|
// Remove from cache so it can be retried
|
|
591
|
-
this.
|
|
742
|
+
await this._clearProcessedMarker(queueId);
|
|
592
743
|
}
|
|
593
744
|
|
|
594
745
|
async moveToDeadLetter(queueId, record, error) {
|
|
@@ -614,22 +765,13 @@ export class S3QueuePlugin extends Plugin {
|
|
|
614
765
|
});
|
|
615
766
|
|
|
616
767
|
// Note: message already in cache from attemptClaim()
|
|
768
|
+
await this._clearProcessedMarker(queueId);
|
|
617
769
|
}
|
|
618
770
|
|
|
619
771
|
async getStats() {
|
|
620
|
-
const [
|
|
621
|
-
this.queueResource.list()
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
if (!ok) {
|
|
625
|
-
if (this.config.verbose) {
|
|
626
|
-
console.warn('[S3QueuePlugin] Failed to get stats:', err.message);
|
|
627
|
-
}
|
|
628
|
-
return null;
|
|
629
|
-
}
|
|
630
|
-
|
|
772
|
+
const statusKeys = ['pending', 'processing', 'completed', 'failed', 'dead'];
|
|
631
773
|
const stats = {
|
|
632
|
-
total:
|
|
774
|
+
total: 0,
|
|
633
775
|
pending: 0,
|
|
634
776
|
processing: 0,
|
|
635
777
|
completed: 0,
|
|
@@ -637,9 +779,29 @@ export class S3QueuePlugin extends Plugin {
|
|
|
637
779
|
dead: 0
|
|
638
780
|
};
|
|
639
781
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
782
|
+
const counts = await Promise.all(
|
|
783
|
+
statusKeys.map(status => tryFn(() => this.queueResource.count({ status })))
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
let derivedTotal = 0;
|
|
787
|
+
|
|
788
|
+
counts.forEach(([ok, err, count], index) => {
|
|
789
|
+
const status = statusKeys[index];
|
|
790
|
+
if (ok) {
|
|
791
|
+
stats[status] = count || 0;
|
|
792
|
+
derivedTotal += count || 0;
|
|
793
|
+
} else if (this.config.verbose) {
|
|
794
|
+
console.warn(`[S3QueuePlugin] Failed to count status '${status}':`, err?.message);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
const [totalOk, totalErr, totalCount] = await tryFn(() => this.queueResource.count());
|
|
799
|
+
if (totalOk) {
|
|
800
|
+
stats.total = totalCount || 0;
|
|
801
|
+
} else {
|
|
802
|
+
stats.total = derivedTotal;
|
|
803
|
+
if (this.config.verbose) {
|
|
804
|
+
console.warn('[S3QueuePlugin] Failed to count total messages:', totalErr?.message);
|
|
643
805
|
}
|
|
644
806
|
}
|
|
645
807
|
|
|
@@ -647,9 +809,12 @@ export class S3QueuePlugin extends Plugin {
|
|
|
647
809
|
}
|
|
648
810
|
|
|
649
811
|
async createDeadLetterResource() {
|
|
812
|
+
if (!this.config.deadLetterResource) return;
|
|
813
|
+
|
|
814
|
+
const resourceName = this.config.deadLetterResource;
|
|
650
815
|
const [ok, err] = await tryFn(() =>
|
|
651
816
|
this.database.createResource({
|
|
652
|
-
name:
|
|
817
|
+
name: resourceName,
|
|
653
818
|
attributes: {
|
|
654
819
|
id: 'string|required',
|
|
655
820
|
originalId: 'string|required',
|
|
@@ -664,12 +829,246 @@ export class S3QueuePlugin extends Plugin {
|
|
|
664
829
|
})
|
|
665
830
|
);
|
|
666
831
|
|
|
667
|
-
if (ok
|
|
668
|
-
this.deadLetterResourceObj = this.database.resources[
|
|
832
|
+
if (ok) {
|
|
833
|
+
this.deadLetterResourceObj = this.database.resources[resourceName];
|
|
834
|
+
} else {
|
|
835
|
+
this.deadLetterResourceObj = this.database.resources[resourceName];
|
|
836
|
+
if (!this.deadLetterResourceObj) {
|
|
837
|
+
throw err;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
this.deadLetterResourceName = this.deadLetterResourceObj.name;
|
|
842
|
+
|
|
843
|
+
if (this.deadLetterResourceAlias) {
|
|
844
|
+
const existing = this.database.resources[this.deadLetterResourceAlias];
|
|
845
|
+
if (!existing || existing === this.deadLetterResourceObj) {
|
|
846
|
+
this.database.resources[this.deadLetterResourceAlias] = this.deadLetterResourceObj;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (this.config.verbose) {
|
|
851
|
+
console.log(`[S3QueuePlugin] Dead letter queue ready: ${this.deadLetterResourceName}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async extendVisibility(queueId, extraMilliseconds) {
|
|
856
|
+
if (!queueId || !extraMilliseconds || extraMilliseconds <= 0) {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
669
859
|
|
|
860
|
+
const [okGet, errGet, entry] = await tryFn(() => this.queueResource.get(queueId));
|
|
861
|
+
if (!okGet || !entry) {
|
|
670
862
|
if (this.config.verbose) {
|
|
671
|
-
console.
|
|
863
|
+
console.warn('[S3QueuePlugin] extendVisibility failed to load entry:', errGet?.message);
|
|
672
864
|
}
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const baseTime = Math.max(entry.visibleAt || 0, Date.now());
|
|
869
|
+
const newVisibleAt = baseTime + extraMilliseconds;
|
|
870
|
+
|
|
871
|
+
const [okUpdate, errUpdate, result] = await tryFn(() =>
|
|
872
|
+
this.queueResource.updateConditional(queueId, {
|
|
873
|
+
visibleAt: newVisibleAt,
|
|
874
|
+
claimedAt: entry.claimedAt || Date.now()
|
|
875
|
+
}, {
|
|
876
|
+
ifMatch: entry._etag
|
|
877
|
+
})
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
if (!okUpdate || !result?.success) {
|
|
881
|
+
if (this.config.verbose) {
|
|
882
|
+
console.warn('[S3QueuePlugin] extendVisibility conditional update failed:', errUpdate?.message || result?.error);
|
|
883
|
+
}
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async recoverStalledMessages(now) {
|
|
891
|
+
if (this.config.recoveryInterval <= 0) return;
|
|
892
|
+
if (this._recoveryInFlight) return;
|
|
893
|
+
if (this._lastRecovery && now - this._lastRecovery < this.config.recoveryInterval) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
this._recoveryInFlight = true;
|
|
898
|
+
this._lastRecovery = now;
|
|
899
|
+
|
|
900
|
+
try {
|
|
901
|
+
const [ok, err, candidates] = await tryFn(() =>
|
|
902
|
+
this.queueResource.query({
|
|
903
|
+
status: 'processing',
|
|
904
|
+
visibleAt: { '<=': now }
|
|
905
|
+
}, {
|
|
906
|
+
limit: this.config.recoveryBatchSize
|
|
907
|
+
})
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
if (!ok) {
|
|
911
|
+
if (this.config.verbose) {
|
|
912
|
+
console.warn('[S3QueuePlugin] Failed to query stalled messages:', err?.message);
|
|
913
|
+
}
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (!candidates || candidates.length === 0) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
for (const candidate of candidates) {
|
|
922
|
+
await this._recoverSingleMessage(candidate, now);
|
|
923
|
+
}
|
|
924
|
+
} finally {
|
|
925
|
+
this._recoveryInFlight = false;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async _recoverSingleMessage(candidate, now) {
|
|
930
|
+
const [okGet, errGet, queueEntry] = await tryFn(() => this.queueResource.get(candidate.id));
|
|
931
|
+
if (!okGet || !queueEntry) {
|
|
932
|
+
if (this.config.verbose) {
|
|
933
|
+
console.warn('[S3QueuePlugin] Failed to load stalled message:', errGet?.message);
|
|
934
|
+
}
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (queueEntry.status !== 'processing' || queueEntry.visibleAt > now) {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// If max attempts reached, move to dead letter
|
|
943
|
+
if (queueEntry.maxAttempts !== undefined && queueEntry.attempts >= queueEntry.maxAttempts) {
|
|
944
|
+
let record = null;
|
|
945
|
+
const [okRecord, , original] = await tryFn(() => this.targetResource.get(queueEntry.originalId));
|
|
946
|
+
if (okRecord && original) {
|
|
947
|
+
record = original;
|
|
948
|
+
} else {
|
|
949
|
+
record = { id: queueEntry.originalId, _missing: true };
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
await this.moveToDeadLetter(queueEntry.id, record, 'visibility-timeout exceeded max attempts');
|
|
953
|
+
this.emit('plg:s3-queue:message-dead', {
|
|
954
|
+
queueId: queueEntry.id,
|
|
955
|
+
originalId: queueEntry.originalId,
|
|
956
|
+
error: 'visibility-timeout exceeded max attempts'
|
|
957
|
+
});
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const [okUpdate, errUpdate, result] = await tryFn(() =>
|
|
962
|
+
this.queueResource.updateConditional(queueEntry.id, {
|
|
963
|
+
status: 'pending',
|
|
964
|
+
visibleAt: now,
|
|
965
|
+
claimedBy: null,
|
|
966
|
+
claimedAt: null,
|
|
967
|
+
error: 'Recovered after visibility timeout'
|
|
968
|
+
}, {
|
|
969
|
+
ifMatch: queueEntry._etag
|
|
970
|
+
})
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
if (!okUpdate || !result?.success) {
|
|
974
|
+
if (this.config.verbose) {
|
|
975
|
+
console.warn('[S3QueuePlugin] Failed to recover message:', errUpdate?.message || result?.error);
|
|
976
|
+
}
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
await this._clearProcessedMarker(queueEntry.id);
|
|
981
|
+
this.emit('plg:s3-queue:message-recovered', {
|
|
982
|
+
queueId: queueEntry.id,
|
|
983
|
+
originalId: queueEntry.originalId
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
_computeIdleDelay(idleStreak) {
|
|
988
|
+
const base = this.config.pollInterval;
|
|
989
|
+
const maxInterval = Math.max(base, this.config.maxPollInterval || base);
|
|
990
|
+
if (maxInterval <= base) {
|
|
991
|
+
return base;
|
|
992
|
+
}
|
|
993
|
+
const factor = Math.pow(2, Math.max(0, idleStreak - 1));
|
|
994
|
+
const delay = base * factor;
|
|
995
|
+
return Math.min(delay, maxInterval);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async _sleep(ms) {
|
|
999
|
+
if (!ms || ms <= 0) return;
|
|
1000
|
+
await new Promise(resolve => setTimeout(resolve, ms));
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
clearProcessedCache() {
|
|
1004
|
+
this.processedCache.clear();
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
async _markMessageProcessed(messageId) {
|
|
1008
|
+
const ttl = Math.max(1000, this.config.processedCacheTTL);
|
|
1009
|
+
const expiresAt = Date.now() + ttl;
|
|
1010
|
+
this.processedCache.set(messageId, expiresAt);
|
|
1011
|
+
|
|
1012
|
+
const storage = this.getStorage();
|
|
1013
|
+
const key = storage.getPluginKey(null, 'cache', 'processed', messageId);
|
|
1014
|
+
const ttlSeconds = Math.max(1, Math.ceil(ttl / 1000));
|
|
1015
|
+
|
|
1016
|
+
const payload = {
|
|
1017
|
+
workerId: this.workerId,
|
|
1018
|
+
markedAt: Date.now()
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
const [ok, err] = await tryFn(() =>
|
|
1022
|
+
storage.set(key, payload, {
|
|
1023
|
+
ttl: ttlSeconds,
|
|
1024
|
+
behavior: 'body-only'
|
|
1025
|
+
})
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
if (!ok && this.config.verbose) {
|
|
1029
|
+
console.warn('[S3QueuePlugin] Failed to persist processed marker:', err?.message);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async _isRecentlyProcessed(messageId) {
|
|
1034
|
+
const now = Date.now();
|
|
1035
|
+
const localExpiresAt = this.processedCache.get(messageId);
|
|
1036
|
+
if (localExpiresAt && localExpiresAt > now) {
|
|
1037
|
+
return true;
|
|
1038
|
+
}
|
|
1039
|
+
if (localExpiresAt && localExpiresAt <= now) {
|
|
1040
|
+
this.processedCache.delete(messageId);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const storage = this.getStorage();
|
|
1044
|
+
const key = storage.getPluginKey(null, 'cache', 'processed', messageId);
|
|
1045
|
+
const [ok, err, data] = await tryFn(() => storage.get(key));
|
|
1046
|
+
|
|
1047
|
+
if (!ok) {
|
|
1048
|
+
if (err && err.code !== 'NoSuchKey' && err.code !== 'NotFound' && this.config.verbose) {
|
|
1049
|
+
console.warn('[S3QueuePlugin] Failed to read processed marker:', err.message || err);
|
|
1050
|
+
}
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (!data) {
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const ttl = Math.max(1000, this.config.processedCacheTTL);
|
|
1059
|
+
this.processedCache.set(messageId, now + ttl);
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
async _clearProcessedMarker(messageId) {
|
|
1064
|
+
this.processedCache.delete(messageId);
|
|
1065
|
+
|
|
1066
|
+
const storage = this.getStorage();
|
|
1067
|
+
const key = storage.getPluginKey(null, 'cache', 'processed', messageId);
|
|
1068
|
+
|
|
1069
|
+
const [ok, err] = await tryFn(() => storage.delete(key));
|
|
1070
|
+
if (!ok && err && err.code !== 'NoSuchKey' && err.code !== 'NotFound' && this.config.verbose) {
|
|
1071
|
+
console.warn('[S3QueuePlugin] Failed to delete processed marker:', err.message || err);
|
|
673
1072
|
}
|
|
674
1073
|
}
|
|
675
1074
|
}
|