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,7 +1,9 @@
|
|
|
1
|
+
import { PromisePool } from "@supercharge/promise-pool";
|
|
1
2
|
import { Plugin } from "./plugin.class.js";
|
|
2
3
|
import tryFn from "../concerns/try-fn.js";
|
|
3
4
|
import { createReplicator, validateReplicatorConfig } from "./replicators/index.js";
|
|
4
5
|
import { ReplicationError } from "./replicator.errors.js";
|
|
6
|
+
import { resolveResourceName } from "./concerns/resource-names.js";
|
|
5
7
|
|
|
6
8
|
function normalizeResourceName(name) {
|
|
7
9
|
return typeof name === 'string' ? name.trim().toLowerCase() : name;
|
|
@@ -118,7 +120,7 @@ function normalizeResourceName(name) {
|
|
|
118
120
|
*/
|
|
119
121
|
export class ReplicatorPlugin extends Plugin {
|
|
120
122
|
constructor(options = {}) {
|
|
121
|
-
super();
|
|
123
|
+
super(options);
|
|
122
124
|
// Validation for config tests
|
|
123
125
|
if (!options.replicators || !Array.isArray(options.replicators)) {
|
|
124
126
|
throw new ReplicationError('ReplicatorPlugin requires replicators array', {
|
|
@@ -156,18 +158,32 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
156
158
|
});
|
|
157
159
|
}
|
|
158
160
|
}
|
|
159
|
-
|
|
161
|
+
|
|
162
|
+
const resourceNamesOption = options.resourceNames || {};
|
|
163
|
+
const resolvedReplicatorConcurrency = Number.isFinite(options.replicatorConcurrency)
|
|
164
|
+
? Math.max(1, Math.floor(options.replicatorConcurrency))
|
|
165
|
+
: 5;
|
|
166
|
+
const resolvedStopConcurrency = Number.isFinite(options.stopConcurrency)
|
|
167
|
+
? Math.max(1, Math.floor(options.stopConcurrency))
|
|
168
|
+
: resolvedReplicatorConcurrency;
|
|
160
169
|
this.config = {
|
|
161
170
|
replicators: options.replicators || [],
|
|
162
171
|
logErrors: options.logErrors !== false,
|
|
163
|
-
replicatorLogResource: options.replicatorLogResource || 'replicator_log',
|
|
164
172
|
persistReplicatorLog: options.persistReplicatorLog || false,
|
|
165
173
|
enabled: options.enabled !== false,
|
|
166
174
|
batchSize: options.batchSize || 100,
|
|
167
175
|
maxRetries: options.maxRetries || 3,
|
|
168
176
|
timeout: options.timeout || 30000,
|
|
169
|
-
verbose: options.verbose || false
|
|
177
|
+
verbose: options.verbose || false,
|
|
178
|
+
replicatorConcurrency: resolvedReplicatorConcurrency,
|
|
179
|
+
stopConcurrency: resolvedStopConcurrency
|
|
180
|
+
};
|
|
181
|
+
this._logResourceDescriptor = {
|
|
182
|
+
defaultName: 'plg_replicator_logs',
|
|
183
|
+
override: resourceNamesOption.log || options.replicatorLogResource
|
|
170
184
|
};
|
|
185
|
+
this.logResourceName = this._resolveLogResourceName();
|
|
186
|
+
this.config.logResourceName = this.logResourceName;
|
|
171
187
|
|
|
172
188
|
this.replicators = [];
|
|
173
189
|
this.database = null;
|
|
@@ -179,6 +195,21 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
179
195
|
lastSync: null
|
|
180
196
|
};
|
|
181
197
|
this._afterCreateResourceHook = null;
|
|
198
|
+
this.replicatorLog = null;
|
|
199
|
+
this._logResourceHooksInstalled = false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
_resolveLogResourceName() {
|
|
203
|
+
return resolveResourceName('replicator', this._logResourceDescriptor, {
|
|
204
|
+
namespace: this.namespace
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
onNamespaceChanged() {
|
|
209
|
+
this.logResourceName = this._resolveLogResourceName();
|
|
210
|
+
if (this.config) {
|
|
211
|
+
this.config.logResourceName = this.logResourceName;
|
|
212
|
+
}
|
|
182
213
|
}
|
|
183
214
|
|
|
184
215
|
// Helper to filter out internal S3DB fields
|
|
@@ -193,24 +224,41 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
193
224
|
return filtered;
|
|
194
225
|
}
|
|
195
226
|
|
|
227
|
+
async prepareReplicationData(resource, data) {
|
|
228
|
+
const complete = await this.getCompleteData(resource, data);
|
|
229
|
+
return this.filterInternalFields(complete);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
sanitizeBeforeData(beforeData) {
|
|
233
|
+
if (!beforeData) return null;
|
|
234
|
+
return this.filterInternalFields(beforeData);
|
|
235
|
+
}
|
|
236
|
+
|
|
196
237
|
async getCompleteData(resource, data) {
|
|
197
|
-
// Always get the complete record from the resource to ensure we have all data
|
|
198
|
-
// This handles all behaviors: body-overflow, truncate-data, body-only, etc.
|
|
199
238
|
const [ok, err, completeRecord] = await tryFn(() => resource.get(data.id));
|
|
200
|
-
|
|
239
|
+
if (ok && completeRecord) {
|
|
240
|
+
return completeRecord;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (this.config.verbose) {
|
|
244
|
+
const reason = err?.message || 'record not found';
|
|
245
|
+
console.warn(`[ReplicatorPlugin] Falling back to provided data for ${resource?.name || 'unknown'}#${data?.id}: ${reason}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return data;
|
|
201
249
|
}
|
|
202
250
|
|
|
203
251
|
installEventListeners(resource, database, plugin) {
|
|
204
252
|
if (!resource || this.eventListenersInstalled.has(resource.name) ||
|
|
205
|
-
resource.name === this.
|
|
253
|
+
resource.name === this.logResourceName) {
|
|
206
254
|
return;
|
|
207
255
|
}
|
|
208
256
|
|
|
209
257
|
// Create handler functions and save references for later removal
|
|
210
258
|
const insertHandler = async (data) => {
|
|
211
259
|
const [ok, error] = await tryFn(async () => {
|
|
212
|
-
const
|
|
213
|
-
await plugin.processReplicatorEvent('insert', resource.name,
|
|
260
|
+
const payload = await plugin.prepareReplicationData(resource, data);
|
|
261
|
+
await plugin.processReplicatorEvent('insert', resource.name, payload.id, payload);
|
|
214
262
|
});
|
|
215
263
|
|
|
216
264
|
if (!ok) {
|
|
@@ -223,10 +271,9 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
223
271
|
|
|
224
272
|
const updateHandler = async (data, beforeData) => {
|
|
225
273
|
const [ok, error] = await tryFn(async () => {
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
await plugin.processReplicatorEvent('update', resource.name, completeData.id, dataWithTimestamp, beforeData);
|
|
274
|
+
const payload = await plugin.prepareReplicationData(resource, data);
|
|
275
|
+
const beforePayload = plugin.sanitizeBeforeData(beforeData);
|
|
276
|
+
await plugin.processReplicatorEvent('update', resource.name, payload.id, payload, beforePayload);
|
|
230
277
|
});
|
|
231
278
|
|
|
232
279
|
if (!ok) {
|
|
@@ -239,7 +286,8 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
239
286
|
|
|
240
287
|
const deleteHandler = async (data) => {
|
|
241
288
|
const [ok, error] = await tryFn(async () => {
|
|
242
|
-
await plugin.
|
|
289
|
+
const sanitized = await plugin.prepareReplicationData(resource, data);
|
|
290
|
+
await plugin.processReplicatorEvent('delete', resource.name, sanitized.id, sanitized);
|
|
243
291
|
});
|
|
244
292
|
|
|
245
293
|
if (!ok) {
|
|
@@ -268,23 +316,41 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
268
316
|
async onInstall() {
|
|
269
317
|
// Create replicator log resource if enabled
|
|
270
318
|
if (this.config.persistReplicatorLog) {
|
|
319
|
+
const logResourceName = this.logResourceName;
|
|
271
320
|
const [ok, err, logResource] = await tryFn(() => this.database.createResource({
|
|
272
|
-
name:
|
|
321
|
+
name: logResourceName,
|
|
273
322
|
attributes: {
|
|
274
323
|
id: 'string|required',
|
|
324
|
+
replicator: 'string|required',
|
|
275
325
|
resource: 'string|required',
|
|
276
326
|
action: 'string|required',
|
|
277
327
|
data: 'json',
|
|
278
328
|
timestamp: 'number|required',
|
|
279
|
-
createdAt: 'string|required'
|
|
329
|
+
createdAt: 'string|required',
|
|
330
|
+
status: 'string|required',
|
|
331
|
+
error: 'string|optional'
|
|
280
332
|
},
|
|
281
|
-
behavior: 'truncate-data'
|
|
333
|
+
behavior: 'truncate-data',
|
|
334
|
+
partitions: {
|
|
335
|
+
byDate: {
|
|
336
|
+
fields: {
|
|
337
|
+
createdAt: 'string|maxlength:10'
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
282
341
|
}));
|
|
283
342
|
|
|
284
343
|
if (ok) {
|
|
285
|
-
this.
|
|
344
|
+
this.replicatorLog = logResource;
|
|
345
|
+
this.installReplicatorLogHooks();
|
|
286
346
|
} else {
|
|
287
|
-
|
|
347
|
+
const existing = this.database.resources[logResourceName];
|
|
348
|
+
if (existing) {
|
|
349
|
+
this.replicatorLog = existing;
|
|
350
|
+
this.installReplicatorLogHooks();
|
|
351
|
+
} else {
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
288
354
|
}
|
|
289
355
|
}
|
|
290
356
|
|
|
@@ -296,7 +362,7 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
296
362
|
|
|
297
363
|
// Install event listeners for existing resources
|
|
298
364
|
for (const resource of Object.values(this.database.resources)) {
|
|
299
|
-
if (resource.name !==
|
|
365
|
+
if (resource.name !== this.logResourceName) {
|
|
300
366
|
this.installEventListeners(resource, this.database, this);
|
|
301
367
|
}
|
|
302
368
|
}
|
|
@@ -309,7 +375,7 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
309
375
|
installDatabaseHooks() {
|
|
310
376
|
// Store hook reference for later removal
|
|
311
377
|
this._afterCreateResourceHook = (resource) => {
|
|
312
|
-
if (resource.name !==
|
|
378
|
+
if (resource.name !== this.logResourceName) {
|
|
313
379
|
this.installEventListeners(resource, this.database, this);
|
|
314
380
|
}
|
|
315
381
|
};
|
|
@@ -325,6 +391,51 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
325
391
|
}
|
|
326
392
|
}
|
|
327
393
|
|
|
394
|
+
installReplicatorLogHooks() {
|
|
395
|
+
if (!this.replicatorLog || typeof this.replicatorLog.addHook !== 'function') {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (this.replicatorLog._replicatorDefaultsInstalled) {
|
|
400
|
+
this._logResourceHooksInstalled = true;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (this._logResourceHooksInstalled) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const ensureInsertDefaults = (data) => {
|
|
409
|
+
if (!data || typeof data !== 'object') {
|
|
410
|
+
return data;
|
|
411
|
+
}
|
|
412
|
+
this._normalizeLogEntry(data, { assignId: true, ensureTimestamp: true });
|
|
413
|
+
return data;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const ensureUpdateDefaults = (data) => {
|
|
417
|
+
if (!data || typeof data !== 'object') {
|
|
418
|
+
return data;
|
|
419
|
+
}
|
|
420
|
+
this._normalizeLogEntry(data, { assignId: false, ensureTimestamp: false });
|
|
421
|
+
return data;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const ensurePatchDefaults = (payload) => {
|
|
425
|
+
if (payload && typeof payload === 'object' && payload.fields && typeof payload.fields === 'object') {
|
|
426
|
+
this._normalizeLogEntry(payload.fields, { assignId: false, ensureTimestamp: false });
|
|
427
|
+
}
|
|
428
|
+
return payload;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
this.replicatorLog.addHook('beforeInsert', ensureInsertDefaults);
|
|
432
|
+
this.replicatorLog.addHook('beforeUpdate', ensureUpdateDefaults);
|
|
433
|
+
this.replicatorLog.addHook('beforePatch', ensurePatchDefaults);
|
|
434
|
+
|
|
435
|
+
this.replicatorLog._replicatorDefaultsInstalled = true;
|
|
436
|
+
this._logResourceHooksInstalled = true;
|
|
437
|
+
}
|
|
438
|
+
|
|
328
439
|
createReplicator(driver, config, resources, client) {
|
|
329
440
|
return createReplicator(driver, config, resources, client);
|
|
330
441
|
}
|
|
@@ -381,21 +492,108 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
381
492
|
throw lastError;
|
|
382
493
|
}
|
|
383
494
|
|
|
495
|
+
_generateLogEntryId() {
|
|
496
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
497
|
+
return `repl-${Date.now()}-${random}`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
_normalizeLogEntry(entry, options = {}) {
|
|
501
|
+
if (!entry || typeof entry !== 'object') {
|
|
502
|
+
return entry;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const {
|
|
506
|
+
assignId = false,
|
|
507
|
+
ensureTimestamp = false
|
|
508
|
+
} = options;
|
|
509
|
+
|
|
510
|
+
if (assignId && !entry.id) {
|
|
511
|
+
entry.id = this._generateLogEntryId();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const numericTimestamp = Number(entry.timestamp);
|
|
515
|
+
const hasNumericTimestamp = Number.isFinite(numericTimestamp);
|
|
516
|
+
if (hasNumericTimestamp) {
|
|
517
|
+
entry.timestamp = numericTimestamp;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (ensureTimestamp && !hasNumericTimestamp) {
|
|
521
|
+
entry.timestamp = Date.now();
|
|
522
|
+
} else if (!ensureTimestamp && entry.timestamp !== undefined && !hasNumericTimestamp) {
|
|
523
|
+
entry.timestamp = Date.now();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (!entry.createdAt && entry.timestamp) {
|
|
527
|
+
const iso = new Date(entry.timestamp).toISOString();
|
|
528
|
+
entry.createdAt = iso.slice(0, 10);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (ensureTimestamp && !entry.createdAt) {
|
|
532
|
+
const iso = new Date().toISOString();
|
|
533
|
+
entry.createdAt = iso.slice(0, 10);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (entry.resourceName || entry.resource) {
|
|
537
|
+
const normalized = normalizeResourceName(entry.resourceName || entry.resource);
|
|
538
|
+
if (normalized) {
|
|
539
|
+
entry.resourceName = normalized;
|
|
540
|
+
if (!entry.resource) {
|
|
541
|
+
entry.resource = normalized;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (!entry.action && entry.operation) {
|
|
547
|
+
entry.action = entry.operation;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (!entry.operation && entry.action) {
|
|
551
|
+
entry.operation = entry.action;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (!entry.replicator) {
|
|
555
|
+
entry.replicator = entry.replicatorId || 'unknown';
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let retryCount = entry.retryCount;
|
|
559
|
+
if (typeof retryCount !== 'number') {
|
|
560
|
+
retryCount = Number(retryCount);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (!Number.isFinite(retryCount) || retryCount < 0) {
|
|
564
|
+
entry.retryCount = 0;
|
|
565
|
+
} else {
|
|
566
|
+
entry.retryCount = Math.floor(retryCount);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (!entry.status) {
|
|
570
|
+
entry.status = 'pending';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (!('error' in entry)) {
|
|
574
|
+
entry.error = null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return entry;
|
|
578
|
+
}
|
|
579
|
+
|
|
384
580
|
async logError(replicator, resourceName, operation, recordId, data, error) {
|
|
385
581
|
const [ok, logError] = await tryFn(async () => {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
582
|
+
if (this.replicatorLog) {
|
|
583
|
+
const logEntry = {
|
|
584
|
+
id: recordId ? `${resourceName}-${recordId}-${Date.now()}` : undefined,
|
|
585
|
+
replicator: replicator.name || replicator.id || 'unknown',
|
|
586
|
+
resource: resourceName,
|
|
391
587
|
resourceName,
|
|
588
|
+
action: operation,
|
|
392
589
|
operation,
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
error: error
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
});
|
|
590
|
+
data: data ? this.filterInternalFields(data) : null,
|
|
591
|
+
status: 'failed',
|
|
592
|
+
error: error?.message,
|
|
593
|
+
retryCount: 0
|
|
594
|
+
};
|
|
595
|
+
this._normalizeLogEntry(logEntry, { assignId: true, ensureTimestamp: true });
|
|
596
|
+
await this.replicatorLog.insert(logEntry);
|
|
399
597
|
}
|
|
400
598
|
});
|
|
401
599
|
|
|
@@ -417,6 +615,18 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
417
615
|
async processReplicatorEvent(operation, resourceName, recordId, data, beforeData = null) {
|
|
418
616
|
if (!this.config.enabled) return;
|
|
419
617
|
|
|
618
|
+
if (!recordId) {
|
|
619
|
+
throw new ReplicationError('Replication event missing record identifier', {
|
|
620
|
+
operation,
|
|
621
|
+
resourceName,
|
|
622
|
+
pluginName: 'ReplicatorPlugin',
|
|
623
|
+
suggestion: 'Ensure the replicated record contains an id before emitting change events.'
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const sanitizedData = data ? this.filterInternalFields(data) : null;
|
|
628
|
+
const sanitizedBefore = beforeData ? this.filterInternalFields(beforeData) : null;
|
|
629
|
+
|
|
420
630
|
const applicableReplicators = this.replicators.filter(replicator => {
|
|
421
631
|
const should = replicator.shouldReplicateResource && replicator.shouldReplicateResource(resourceName, operation);
|
|
422
632
|
return should;
|
|
@@ -426,32 +636,42 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
426
636
|
return;
|
|
427
637
|
}
|
|
428
638
|
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
639
|
+
const entries = applicableReplicators.map((replicator, index) => ({ replicator, index }));
|
|
640
|
+
const outcomes = new Array(entries.length);
|
|
641
|
+
|
|
642
|
+
const poolResult = await PromisePool
|
|
643
|
+
.withConcurrency(this.config.replicatorConcurrency)
|
|
644
|
+
.for(entries)
|
|
645
|
+
.process(async ({ replicator, index }) => {
|
|
646
|
+
const [ok, error, replicationResult] = await tryFn(async () => {
|
|
647
|
+
const result = await this.retryWithBackoff(
|
|
648
|
+
() => replicator.replicate(resourceName, operation, sanitizedData, recordId, sanitizedBefore),
|
|
649
|
+
this.config.maxRetries
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
this.emit('plg:replicator:replicated', {
|
|
653
|
+
replicator: replicator.name || replicator.id,
|
|
654
|
+
resourceName,
|
|
655
|
+
operation,
|
|
656
|
+
recordId,
|
|
657
|
+
result,
|
|
658
|
+
success: true
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
this.stats.totalReplications += 1;
|
|
662
|
+
|
|
663
|
+
return result;
|
|
443
664
|
});
|
|
444
665
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
} else {
|
|
666
|
+
if (ok) {
|
|
667
|
+
outcomes[index] = { status: 'fulfilled', value: replicationResult };
|
|
668
|
+
return replicationResult;
|
|
669
|
+
}
|
|
670
|
+
|
|
451
671
|
if (this.config.verbose) {
|
|
452
672
|
console.warn(`[ReplicatorPlugin] Replication failed for ${replicator.name || replicator.id} on ${resourceName}: ${error.message}`);
|
|
453
673
|
}
|
|
454
|
-
|
|
674
|
+
|
|
455
675
|
this.emit('plg:replicator:error', {
|
|
456
676
|
replicator: replicator.name || replicator.id,
|
|
457
677
|
resourceName,
|
|
@@ -460,15 +680,25 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
460
680
|
error: error.message
|
|
461
681
|
});
|
|
462
682
|
|
|
683
|
+
this.stats.totalErrors += 1;
|
|
684
|
+
|
|
463
685
|
if (this.config.logErrors && this.database) {
|
|
464
686
|
await this.logError(replicator, resourceName, operation, recordId, data, error);
|
|
465
687
|
}
|
|
466
688
|
|
|
689
|
+
outcomes[index] = { status: 'rejected', reason: error };
|
|
467
690
|
throw error;
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
if (poolResult.errors.length > 0) {
|
|
694
|
+
for (const { item, error } of poolResult.errors) {
|
|
695
|
+
if (item && typeof item.index === 'number' && !outcomes[item.index]) {
|
|
696
|
+
outcomes[item.index] = { status: 'rejected', reason: error };
|
|
697
|
+
}
|
|
468
698
|
}
|
|
469
|
-
}
|
|
699
|
+
}
|
|
470
700
|
|
|
471
|
-
return
|
|
701
|
+
return outcomes;
|
|
472
702
|
}
|
|
473
703
|
|
|
474
704
|
async processReplicatorItem(item) {
|
|
@@ -481,51 +711,64 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
481
711
|
return;
|
|
482
712
|
}
|
|
483
713
|
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
714
|
+
const entries = applicableReplicators.map((replicator, index) => ({ replicator, index }));
|
|
715
|
+
const outcomes = new Array(entries.length);
|
|
716
|
+
|
|
717
|
+
await PromisePool
|
|
718
|
+
.withConcurrency(this.config.replicatorConcurrency)
|
|
719
|
+
.for(entries)
|
|
720
|
+
.process(async ({ replicator, index }) => {
|
|
721
|
+
const [wrapperOk, wrapperError] = await tryFn(async () => {
|
|
722
|
+
const preparedData = item.data ? this.filterInternalFields(item.data) : null;
|
|
723
|
+
const preparedBefore = item.beforeData ? this.filterInternalFields(item.beforeData) : null;
|
|
724
|
+
const [ok, err, result] = await tryFn(() =>
|
|
725
|
+
replicator.replicate(item.resourceName, item.operation, preparedData, item.recordId, preparedBefore)
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
if (!ok) {
|
|
729
|
+
if (this.config.verbose) {
|
|
730
|
+
console.warn(`[ReplicatorPlugin] Replicator item processing failed for ${replicator.name || replicator.id} on ${item.resourceName}: ${err.message}`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
this.emit('plg:replicator:error', {
|
|
734
|
+
replicator: replicator.name || replicator.id,
|
|
735
|
+
resourceName: item.resourceName,
|
|
736
|
+
operation: item.operation,
|
|
737
|
+
recordId: item.recordId,
|
|
738
|
+
error: err.message
|
|
739
|
+
});
|
|
489
740
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
741
|
+
if (this.config.logErrors && this.database) {
|
|
742
|
+
await this.logError(replicator, item.resourceName, item.operation, item.recordId, item.data, err);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
this.stats.totalErrors += 1;
|
|
746
|
+
return { success: false, error: err.message };
|
|
493
747
|
}
|
|
494
|
-
|
|
495
|
-
this.emit('plg:replicator:
|
|
748
|
+
|
|
749
|
+
this.emit('plg:replicator:replicated', {
|
|
496
750
|
replicator: replicator.name || replicator.id,
|
|
497
751
|
resourceName: item.resourceName,
|
|
498
752
|
operation: item.operation,
|
|
499
753
|
recordId: item.recordId,
|
|
500
|
-
|
|
754
|
+
result,
|
|
755
|
+
success: true
|
|
501
756
|
});
|
|
502
757
|
|
|
503
|
-
|
|
504
|
-
await this.logError(replicator, item.resourceName, item.operation, item.recordId, item.data, err);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return { success: false, error: err.message };
|
|
508
|
-
}
|
|
758
|
+
this.stats.totalReplications += 1;
|
|
509
759
|
|
|
510
|
-
|
|
511
|
-
replicator: replicator.name || replicator.id,
|
|
512
|
-
resourceName: item.resourceName,
|
|
513
|
-
operation: item.operation,
|
|
514
|
-
recordId: item.recordId,
|
|
515
|
-
result,
|
|
516
|
-
success: true
|
|
760
|
+
return { success: true, result };
|
|
517
761
|
});
|
|
518
762
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
} else {
|
|
763
|
+
if (wrapperOk) {
|
|
764
|
+
outcomes[index] = { status: 'fulfilled', value: wrapperOk };
|
|
765
|
+
return wrapperOk;
|
|
766
|
+
}
|
|
767
|
+
|
|
525
768
|
if (this.config.verbose) {
|
|
526
769
|
console.warn(`[ReplicatorPlugin] Wrapper processing failed for ${replicator.name || replicator.id} on ${item.resourceName}: ${wrapperError.message}`);
|
|
527
770
|
}
|
|
528
|
-
|
|
771
|
+
|
|
529
772
|
this.emit('plg:replicator:error', {
|
|
530
773
|
replicator: replicator.name || replicator.id,
|
|
531
774
|
resourceName: item.resourceName,
|
|
@@ -538,29 +781,45 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
538
781
|
await this.logError(replicator, item.resourceName, item.operation, item.recordId, item.data, wrapperError);
|
|
539
782
|
}
|
|
540
783
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
784
|
+
this.stats.totalErrors += 1;
|
|
785
|
+
const failure = { success: false, error: wrapperError.message };
|
|
786
|
+
outcomes[index] = { status: 'fulfilled', value: failure };
|
|
787
|
+
return failure;
|
|
788
|
+
});
|
|
544
789
|
|
|
545
|
-
return
|
|
790
|
+
return outcomes;
|
|
546
791
|
}
|
|
547
792
|
|
|
548
793
|
async logReplicator(item) {
|
|
549
794
|
// Always use the saved reference
|
|
550
|
-
const logRes = this.replicatorLog
|
|
795
|
+
const logRes = this.replicatorLog;
|
|
551
796
|
if (!logRes) {
|
|
552
797
|
this.emit('plg:replicator:log-failed', { error: 'replicator log resource not found', item });
|
|
553
798
|
return;
|
|
554
799
|
}
|
|
555
|
-
|
|
800
|
+
const sanitizedData = item.data ? this.filterInternalFields(item.data) : {};
|
|
801
|
+
|
|
802
|
+
// Fix required fields of log resource
|
|
556
803
|
const logItem = {
|
|
557
|
-
id: item.id
|
|
804
|
+
id: item.id,
|
|
805
|
+
replicator: item.replicator || 'unknown',
|
|
558
806
|
resource: item.resource || item.resourceName || '',
|
|
807
|
+
resourceName: item.resourceName || item.resource || '',
|
|
559
808
|
action: item.operation || item.action || '',
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
809
|
+
operation: item.operation || item.action || '',
|
|
810
|
+
data: sanitizedData,
|
|
811
|
+
status: item.status || 'pending',
|
|
812
|
+
error: item.error || null,
|
|
813
|
+
retryCount: item.retryCount || 0
|
|
563
814
|
};
|
|
815
|
+
if (typeof item.timestamp === 'number') {
|
|
816
|
+
logItem.timestamp = item.timestamp;
|
|
817
|
+
}
|
|
818
|
+
if (item.createdAt) {
|
|
819
|
+
logItem.createdAt = item.createdAt;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
this._normalizeLogEntry(logItem, { assignId: true, ensureTimestamp: true });
|
|
564
823
|
const [ok, err] = await tryFn(async () => {
|
|
565
824
|
await logRes.insert(logItem);
|
|
566
825
|
});
|
|
@@ -590,17 +849,35 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
590
849
|
|
|
591
850
|
// Utility methods
|
|
592
851
|
async getReplicatorStats() {
|
|
593
|
-
const
|
|
594
|
-
|
|
852
|
+
const entries = this.replicators.map((replicator, index) => ({ replicator, index }));
|
|
853
|
+
const replicatorStats = new Array(entries.length);
|
|
854
|
+
|
|
855
|
+
const poolResult = await PromisePool
|
|
856
|
+
.withConcurrency(this.config.replicatorConcurrency)
|
|
857
|
+
.for(entries)
|
|
858
|
+
.process(async ({ replicator, index }) => {
|
|
595
859
|
const status = await replicator.getStatus();
|
|
596
|
-
|
|
860
|
+
const info = {
|
|
597
861
|
id: replicator.id,
|
|
598
862
|
driver: replicator.driver,
|
|
599
863
|
config: replicator.config,
|
|
600
864
|
status
|
|
601
865
|
};
|
|
602
|
-
|
|
603
|
-
|
|
866
|
+
replicatorStats[index] = info;
|
|
867
|
+
return info;
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
if (poolResult.errors.length > 0) {
|
|
871
|
+
const { item, error } = poolResult.errors[0];
|
|
872
|
+
const failedReplicator = item?.replicator;
|
|
873
|
+
throw new ReplicationError(`Failed to collect status for replicator ${failedReplicator?.name || failedReplicator?.id || 'unknown'}`, {
|
|
874
|
+
operation: 'getReplicatorStats',
|
|
875
|
+
pluginName: 'ReplicatorPlugin',
|
|
876
|
+
replicatorId: failedReplicator?.id,
|
|
877
|
+
driver: failedReplicator?.driver,
|
|
878
|
+
original: error
|
|
879
|
+
});
|
|
880
|
+
}
|
|
604
881
|
|
|
605
882
|
return {
|
|
606
883
|
replicators: replicatorStats,
|
|
@@ -652,20 +929,75 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
652
929
|
|
|
653
930
|
let retried = 0;
|
|
654
931
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
932
|
+
const processResult = await PromisePool
|
|
933
|
+
.withConcurrency(this.config.replicatorConcurrency)
|
|
934
|
+
.for(failedLogs || [])
|
|
935
|
+
.process(async (log) => {
|
|
936
|
+
const sanitizedData = log.data ? this.filterInternalFields(log.data) : null;
|
|
937
|
+
const sanitizedBefore = log.beforeData ? this.filterInternalFields(log.beforeData) : null;
|
|
938
|
+
|
|
939
|
+
const [ok, err, results] = await tryFn(async () => {
|
|
940
|
+
return await this.processReplicatorEvent(
|
|
941
|
+
log.operation,
|
|
942
|
+
log.resourceName,
|
|
943
|
+
log.recordId,
|
|
944
|
+
sanitizedData,
|
|
945
|
+
sanitizedBefore
|
|
946
|
+
);
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
const isSuccessfulEntry = (entry) => {
|
|
950
|
+
if (!entry || entry.status !== 'fulfilled') {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
if (entry.value && typeof entry.value === 'object' && 'success' in entry.value) {
|
|
954
|
+
return entry.value.success !== false;
|
|
955
|
+
}
|
|
956
|
+
return true;
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
if (ok && Array.isArray(results) && results.every(isSuccessfulEntry)) {
|
|
960
|
+
retried += 1;
|
|
961
|
+
await this.updateReplicatorLog(log.id, {
|
|
962
|
+
status: 'success',
|
|
963
|
+
error: null,
|
|
964
|
+
retryCount: log.retryCount || 0,
|
|
965
|
+
lastSuccessAt: new Date().toISOString()
|
|
966
|
+
});
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
let failureMessage = err?.message || 'Unknown replication failure';
|
|
971
|
+
|
|
972
|
+
if (Array.isArray(results)) {
|
|
973
|
+
const failureEntry = results.find((entry) => {
|
|
974
|
+
if (!entry) return false;
|
|
975
|
+
if (entry.status === 'rejected') return true;
|
|
976
|
+
if (entry.status === 'fulfilled' && entry.value && typeof entry.value === 'object' && 'success' in entry.value) {
|
|
977
|
+
return entry.value.success === false;
|
|
978
|
+
}
|
|
979
|
+
return false;
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
if (failureEntry) {
|
|
983
|
+
if (failureEntry.status === 'rejected') {
|
|
984
|
+
failureMessage = failureEntry.reason?.message || failureMessage;
|
|
985
|
+
} else if (failureEntry.status === 'fulfilled') {
|
|
986
|
+
failureMessage = failureEntry.value?.error || failureMessage;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
await this.updateReplicatorLog(log.id, {
|
|
992
|
+
status: 'failed',
|
|
993
|
+
error: failureMessage,
|
|
994
|
+
retryCount: (Number(log.retryCount) || 0) + 1
|
|
995
|
+
});
|
|
664
996
|
});
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
|
|
997
|
+
|
|
998
|
+
if (processResult.errors.length && this.config.verbose) {
|
|
999
|
+
for (const { item, error } of processResult.errors) {
|
|
1000
|
+
console.warn(`[ReplicatorPlugin] Failed to retry log ${item?.id ?? 'unknown'}: ${error.message}`);
|
|
669
1001
|
}
|
|
670
1002
|
}
|
|
671
1003
|
|
|
@@ -687,7 +1019,7 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
687
1019
|
this.stats.lastSync = new Date().toISOString();
|
|
688
1020
|
|
|
689
1021
|
for (const resourceName in this.database.resources) {
|
|
690
|
-
if (
|
|
1022
|
+
if (resourceName === this.logResourceName) continue;
|
|
691
1023
|
|
|
692
1024
|
if (replicator.shouldReplicateResource(resourceName)) {
|
|
693
1025
|
this.emit('plg:replicator:sync-resource', { resourceName, replicatorId });
|
|
@@ -706,8 +1038,52 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
706
1038
|
const records = Array.isArray(page) ? page : (page.items || []);
|
|
707
1039
|
if (records.length === 0) break;
|
|
708
1040
|
|
|
709
|
-
|
|
710
|
-
|
|
1041
|
+
const poolResult = await PromisePool
|
|
1042
|
+
.withConcurrency(this.config.replicatorConcurrency)
|
|
1043
|
+
.for(records)
|
|
1044
|
+
.process(async (record) => {
|
|
1045
|
+
const sanitizedRecord = this.filterInternalFields(record);
|
|
1046
|
+
const [replicateOk, replicateError, result] = await tryFn(() =>
|
|
1047
|
+
replicator.replicate(resourceName, 'insert', sanitizedRecord, sanitizedRecord.id)
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1050
|
+
if (!replicateOk) {
|
|
1051
|
+
if (this.config.verbose) {
|
|
1052
|
+
console.warn(`[ReplicatorPlugin] syncAllData failed for ${replicator.name || replicator.id} on ${resourceName}: ${replicateError.message}`);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
this.stats.totalErrors += 1;
|
|
1056
|
+
this.emit('plg:replicator:error', {
|
|
1057
|
+
replicator: replicator.name || replicator.id,
|
|
1058
|
+
resourceName,
|
|
1059
|
+
operation: 'insert',
|
|
1060
|
+
recordId: sanitizedRecord.id,
|
|
1061
|
+
error: replicateError.message
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
if (this.config.logErrors && this.database) {
|
|
1065
|
+
await this.logError(replicator, resourceName, 'insert', sanitizedRecord.id, sanitizedRecord, replicateError);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
throw replicateError;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
this.stats.totalReplications += 1;
|
|
1072
|
+
this.emit('plg:replicator:replicated', {
|
|
1073
|
+
replicator: replicator.name || replicator.id,
|
|
1074
|
+
resourceName,
|
|
1075
|
+
operation: 'insert',
|
|
1076
|
+
recordId: sanitizedRecord.id,
|
|
1077
|
+
result,
|
|
1078
|
+
success: true
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
return result;
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
if (poolResult.errors.length > 0) {
|
|
1085
|
+
const { error } = poolResult.errors[0];
|
|
1086
|
+
throw error;
|
|
711
1087
|
}
|
|
712
1088
|
|
|
713
1089
|
offset += pageSize;
|
|
@@ -721,26 +1097,27 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
721
1097
|
async stop() {
|
|
722
1098
|
const [ok, error] = await tryFn(async () => {
|
|
723
1099
|
if (this.replicators && this.replicators.length > 0) {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1100
|
+
await PromisePool
|
|
1101
|
+
.withConcurrency(this.config.stopConcurrency)
|
|
1102
|
+
.for(this.replicators)
|
|
1103
|
+
.process(async (replicator) => {
|
|
1104
|
+
const [replicatorOk, replicatorError] = await tryFn(async () => {
|
|
1105
|
+
if (replicator && typeof replicator.stop === 'function') {
|
|
1106
|
+
await replicator.stop();
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
730
1109
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1110
|
+
if (!replicatorOk) {
|
|
1111
|
+
if (this.config.verbose) {
|
|
1112
|
+
console.warn(`[ReplicatorPlugin] Failed to stop replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
|
|
1113
|
+
}
|
|
1114
|
+
this.emit('plg:replicator:stop-error', {
|
|
1115
|
+
replicator: replicator.name || replicator.id || 'unknown',
|
|
1116
|
+
driver: replicator.driver || 'unknown',
|
|
1117
|
+
error: replicatorError.message
|
|
1118
|
+
});
|
|
734
1119
|
}
|
|
735
|
-
|
|
736
|
-
replicator: replicator.name || replicator.id || 'unknown',
|
|
737
|
-
driver: replicator.driver || 'unknown',
|
|
738
|
-
error: replicatorError.message
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
await Promise.allSettled(cleanupPromises);
|
|
1120
|
+
});
|
|
744
1121
|
}
|
|
745
1122
|
|
|
746
1123
|
// Remove database hooks
|
|
@@ -777,4 +1154,4 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
777
1154
|
});
|
|
778
1155
|
}
|
|
779
1156
|
}
|
|
780
|
-
}
|
|
1157
|
+
}
|