s3db.js 10.0.0 → 10.0.3
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 +2 -1
- package/dist/s3db.cjs.js +2204 -612
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +70 -3
- package/dist/s3db.es.js +2203 -613
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/client.class.js +6 -5
- package/src/plugins/audit.plugin.js +8 -6
- package/src/plugins/backup.plugin.js +383 -106
- package/src/plugins/cache.plugin.js +203 -150
- package/src/plugins/eventual-consistency.plugin.js +609 -206
- package/src/plugins/fulltext.plugin.js +6 -6
- package/src/plugins/index.js +1 -0
- package/src/plugins/metrics.plugin.js +13 -13
- package/src/plugins/queue-consumer.plugin.js +4 -2
- package/src/plugins/replicator.plugin.js +108 -70
- package/src/plugins/replicators/s3db-replicator.class.js +7 -3
- package/src/plugins/replicators/sqs-replicator.class.js +11 -3
- package/src/plugins/s3-queue.plugin.js +776 -0
- package/src/plugins/scheduler.plugin.js +226 -164
- package/src/plugins/state-machine.plugin.js +109 -81
- package/src/resource.class.js +205 -0
- package/src/s3db.d.ts +70 -3
|
@@ -18,7 +18,7 @@ export class FullTextPlugin extends Plugin {
|
|
|
18
18
|
|
|
19
19
|
// Create index resource if it doesn't exist
|
|
20
20
|
const [ok, err, indexResource] = await tryFn(() => database.createResource({
|
|
21
|
-
name: '
|
|
21
|
+
name: 'plg_fulltext_indexes',
|
|
22
22
|
attributes: {
|
|
23
23
|
id: 'string|required',
|
|
24
24
|
resourceName: 'string|required',
|
|
@@ -96,7 +96,7 @@ export class FullTextPlugin extends Plugin {
|
|
|
96
96
|
installDatabaseHooks() {
|
|
97
97
|
// Use the new database hooks system for automatic resource discovery
|
|
98
98
|
this.database.addHook('afterCreateResource', (resource) => {
|
|
99
|
-
if (resource.name !== '
|
|
99
|
+
if (resource.name !== 'plg_fulltext_indexes') {
|
|
100
100
|
this.installResourceHooks(resource);
|
|
101
101
|
}
|
|
102
102
|
});
|
|
@@ -115,7 +115,7 @@ export class FullTextPlugin extends Plugin {
|
|
|
115
115
|
this.database.plugins.fulltext = this;
|
|
116
116
|
|
|
117
117
|
for (const resource of Object.values(this.database.resources)) {
|
|
118
|
-
if (resource.name === '
|
|
118
|
+
if (resource.name === 'plg_fulltext_indexes') continue;
|
|
119
119
|
|
|
120
120
|
this.installResourceHooks(resource);
|
|
121
121
|
}
|
|
@@ -126,7 +126,7 @@ export class FullTextPlugin extends Plugin {
|
|
|
126
126
|
this.database._previousCreateResourceForFullText = this.database.createResource;
|
|
127
127
|
this.database.createResource = async function (...args) {
|
|
128
128
|
const resource = await this._previousCreateResourceForFullText(...args);
|
|
129
|
-
if (this.plugins?.fulltext && resource.name !== '
|
|
129
|
+
if (this.plugins?.fulltext && resource.name !== 'plg_fulltext_indexes') {
|
|
130
130
|
this.plugins.fulltext.installResourceHooks(resource);
|
|
131
131
|
}
|
|
132
132
|
return resource;
|
|
@@ -136,7 +136,7 @@ export class FullTextPlugin extends Plugin {
|
|
|
136
136
|
|
|
137
137
|
// Ensure all existing resources have hooks (even if created before plugin setup)
|
|
138
138
|
for (const resource of Object.values(this.database.resources)) {
|
|
139
|
-
if (resource.name !== '
|
|
139
|
+
if (resource.name !== 'plg_fulltext_indexes') {
|
|
140
140
|
this.installResourceHooks(resource);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
@@ -460,7 +460,7 @@ export class FullTextPlugin extends Plugin {
|
|
|
460
460
|
}
|
|
461
461
|
|
|
462
462
|
async _rebuildAllIndexesInternal() {
|
|
463
|
-
const resourceNames = Object.keys(this.database.resources).filter(name => name !== '
|
|
463
|
+
const resourceNames = Object.keys(this.database.resources).filter(name => name !== 'plg_fulltext_indexes');
|
|
464
464
|
|
|
465
465
|
// Process resources sequentially to avoid overwhelming the system
|
|
466
466
|
for (const resourceName of resourceNames) {
|
package/src/plugins/index.js
CHANGED
|
@@ -12,5 +12,6 @@ export * from './fulltext.plugin.js'
|
|
|
12
12
|
export * from './metrics.plugin.js'
|
|
13
13
|
export * from './queue-consumer.plugin.js'
|
|
14
14
|
export * from './replicator.plugin.js'
|
|
15
|
+
export * from './s3-queue.plugin.js'
|
|
15
16
|
export * from './scheduler.plugin.js'
|
|
16
17
|
export * from './state-machine.plugin.js'
|
|
@@ -37,7 +37,7 @@ export class MetricsPlugin extends Plugin {
|
|
|
37
37
|
|
|
38
38
|
const [ok, err] = await tryFn(async () => {
|
|
39
39
|
const [ok1, err1, metricsResource] = await tryFn(() => database.createResource({
|
|
40
|
-
name: '
|
|
40
|
+
name: 'plg_metrics',
|
|
41
41
|
attributes: {
|
|
42
42
|
id: 'string|required',
|
|
43
43
|
type: 'string|required', // 'operation', 'error', 'performance'
|
|
@@ -51,10 +51,10 @@ export class MetricsPlugin extends Plugin {
|
|
|
51
51
|
metadata: 'json'
|
|
52
52
|
}
|
|
53
53
|
}));
|
|
54
|
-
this.metricsResource = ok1 ? metricsResource : database.resources.
|
|
54
|
+
this.metricsResource = ok1 ? metricsResource : database.resources.plg_metrics;
|
|
55
55
|
|
|
56
56
|
const [ok2, err2, errorsResource] = await tryFn(() => database.createResource({
|
|
57
|
-
name: '
|
|
57
|
+
name: 'plg_error_logs',
|
|
58
58
|
attributes: {
|
|
59
59
|
id: 'string|required',
|
|
60
60
|
resourceName: 'string|required',
|
|
@@ -64,10 +64,10 @@ export class MetricsPlugin extends Plugin {
|
|
|
64
64
|
metadata: 'json'
|
|
65
65
|
}
|
|
66
66
|
}));
|
|
67
|
-
this.errorsResource = ok2 ? errorsResource : database.resources.
|
|
67
|
+
this.errorsResource = ok2 ? errorsResource : database.resources.plg_error_logs;
|
|
68
68
|
|
|
69
69
|
const [ok3, err3, performanceResource] = await tryFn(() => database.createResource({
|
|
70
|
-
name: '
|
|
70
|
+
name: 'plg_performance_logs',
|
|
71
71
|
attributes: {
|
|
72
72
|
id: 'string|required',
|
|
73
73
|
resourceName: 'string|required',
|
|
@@ -77,13 +77,13 @@ export class MetricsPlugin extends Plugin {
|
|
|
77
77
|
metadata: 'json'
|
|
78
78
|
}
|
|
79
79
|
}));
|
|
80
|
-
this.performanceResource = ok3 ? performanceResource : database.resources.
|
|
80
|
+
this.performanceResource = ok3 ? performanceResource : database.resources.plg_performance_logs;
|
|
81
81
|
});
|
|
82
82
|
if (!ok) {
|
|
83
83
|
// Resources might already exist
|
|
84
|
-
this.metricsResource = database.resources.
|
|
85
|
-
this.errorsResource = database.resources.
|
|
86
|
-
this.performanceResource = database.resources.
|
|
84
|
+
this.metricsResource = database.resources.plg_metrics;
|
|
85
|
+
this.errorsResource = database.resources.plg_error_logs;
|
|
86
|
+
this.performanceResource = database.resources.plg_performance_logs;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// Use database hooks for automatic resource discovery
|
|
@@ -116,7 +116,7 @@ export class MetricsPlugin extends Plugin {
|
|
|
116
116
|
installDatabaseHooks() {
|
|
117
117
|
// Use the new database hooks system for automatic resource discovery
|
|
118
118
|
this.database.addHook('afterCreateResource', (resource) => {
|
|
119
|
-
if (resource.name !== '
|
|
119
|
+
if (resource.name !== 'plg_metrics' && resource.name !== 'plg_error_logs' && resource.name !== 'plg_performance_logs') {
|
|
120
120
|
this.installResourceHooks(resource);
|
|
121
121
|
}
|
|
122
122
|
});
|
|
@@ -130,10 +130,10 @@ export class MetricsPlugin extends Plugin {
|
|
|
130
130
|
installMetricsHooks() {
|
|
131
131
|
// Only hook into non-metrics resources
|
|
132
132
|
for (const resource of Object.values(this.database.resources)) {
|
|
133
|
-
if (['
|
|
133
|
+
if (['plg_metrics', 'plg_error_logs', 'plg_performance_logs'].includes(resource.name)) {
|
|
134
134
|
continue; // Skip metrics resources to avoid recursion
|
|
135
135
|
}
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
this.installResourceHooks(resource);
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -141,7 +141,7 @@ export class MetricsPlugin extends Plugin {
|
|
|
141
141
|
this.database._createResource = this.database.createResource;
|
|
142
142
|
this.database.createResource = async function (...args) {
|
|
143
143
|
const resource = await this._createResource(...args);
|
|
144
|
-
if (this.plugins?.metrics && !['
|
|
144
|
+
if (this.plugins?.metrics && !['plg_metrics', 'plg_error_logs', 'plg_performance_logs'].includes(resource.name)) {
|
|
145
145
|
this.plugins.metrics.installResourceHooks(resource);
|
|
146
146
|
}
|
|
147
147
|
return resource;
|
|
@@ -20,7 +20,7 @@ import tryFn from "../concerns/try-fn.js";
|
|
|
20
20
|
// reconnectInterval: 2000,
|
|
21
21
|
// });
|
|
22
22
|
|
|
23
|
-
export
|
|
23
|
+
export class QueueConsumerPlugin {
|
|
24
24
|
constructor(options = {}) {
|
|
25
25
|
this.options = options;
|
|
26
26
|
// New pattern: consumers = [{ driver, config, consumers: [{ queueUrl, resources, ... }] }]
|
|
@@ -131,4 +131,6 @@ export default class QueueConsumerPlugin {
|
|
|
131
131
|
|
|
132
132
|
_handleError(err, raw, resourceName) {
|
|
133
133
|
}
|
|
134
|
-
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default QueueConsumerPlugin;
|
|
@@ -17,7 +17,7 @@ function normalizeResourceName(name) {
|
|
|
17
17
|
* If true, the plugin will persist all replicator events to a log resource.
|
|
18
18
|
* If false, no replicator log resource is created or used.
|
|
19
19
|
*
|
|
20
|
-
* - replicatorLogResource (string, default: '
|
|
20
|
+
* - replicatorLogResource (string, default: 'plg_replicator_logs')
|
|
21
21
|
* The name of the resource used to store replicator logs.
|
|
22
22
|
*
|
|
23
23
|
* === replicator Log Resource Structure ===
|
|
@@ -132,24 +132,24 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
132
132
|
replicators: options.replicators || [],
|
|
133
133
|
logErrors: options.logErrors !== false,
|
|
134
134
|
replicatorLogResource: options.replicatorLogResource || 'replicator_log',
|
|
135
|
+
persistReplicatorLog: options.persistReplicatorLog || false,
|
|
135
136
|
enabled: options.enabled !== false,
|
|
136
137
|
batchSize: options.batchSize || 100,
|
|
137
138
|
maxRetries: options.maxRetries || 3,
|
|
138
139
|
timeout: options.timeout || 30000,
|
|
139
|
-
verbose: options.verbose || false
|
|
140
|
-
...options
|
|
140
|
+
verbose: options.verbose || false
|
|
141
141
|
};
|
|
142
|
-
|
|
142
|
+
|
|
143
143
|
this.replicators = [];
|
|
144
144
|
this.database = null;
|
|
145
145
|
this.eventListenersInstalled = new Set();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
146
|
+
this.eventHandlers = new Map(); // Map<resourceName, {insert, update, delete}>
|
|
147
|
+
this.stats = {
|
|
148
|
+
totalReplications: 0,
|
|
149
|
+
totalErrors: 0,
|
|
150
|
+
lastSync: null
|
|
151
|
+
};
|
|
152
|
+
this._afterCreateResourceHook = null;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// Helper to filter out internal S3DB fields
|
|
@@ -172,54 +172,67 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
installEventListeners(resource, database, plugin) {
|
|
175
|
-
if (!resource || this.eventListenersInstalled.has(resource.name) ||
|
|
175
|
+
if (!resource || this.eventListenersInstalled.has(resource.name) ||
|
|
176
176
|
resource.name === this.config.replicatorLogResource) {
|
|
177
177
|
return;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
// Create handler functions and save references for later removal
|
|
181
|
+
const insertHandler = async (data) => {
|
|
181
182
|
const [ok, error] = await tryFn(async () => {
|
|
182
183
|
const completeData = { ...data, createdAt: new Date().toISOString() };
|
|
183
184
|
await plugin.processReplicatorEvent('insert', resource.name, completeData.id, completeData);
|
|
184
185
|
});
|
|
185
|
-
|
|
186
|
+
|
|
186
187
|
if (!ok) {
|
|
187
188
|
if (this.config.verbose) {
|
|
188
189
|
console.warn(`[ReplicatorPlugin] Insert event failed for resource ${resource.name}: ${error.message}`);
|
|
189
190
|
}
|
|
190
191
|
this.emit('error', { operation: 'insert', error: error.message, resource: resource.name });
|
|
191
192
|
}
|
|
192
|
-
}
|
|
193
|
+
};
|
|
193
194
|
|
|
194
|
-
|
|
195
|
+
const updateHandler = async (data, beforeData) => {
|
|
195
196
|
const [ok, error] = await tryFn(async () => {
|
|
196
197
|
// For updates, we need to get the complete updated record, not just the changed fields
|
|
197
198
|
const completeData = await plugin.getCompleteData(resource, data);
|
|
198
199
|
const dataWithTimestamp = { ...completeData, updatedAt: new Date().toISOString() };
|
|
199
200
|
await plugin.processReplicatorEvent('update', resource.name, completeData.id, dataWithTimestamp, beforeData);
|
|
200
201
|
});
|
|
201
|
-
|
|
202
|
+
|
|
202
203
|
if (!ok) {
|
|
203
204
|
if (this.config.verbose) {
|
|
204
205
|
console.warn(`[ReplicatorPlugin] Update event failed for resource ${resource.name}: ${error.message}`);
|
|
205
206
|
}
|
|
206
207
|
this.emit('error', { operation: 'update', error: error.message, resource: resource.name });
|
|
207
208
|
}
|
|
208
|
-
}
|
|
209
|
+
};
|
|
209
210
|
|
|
210
|
-
|
|
211
|
+
const deleteHandler = async (data) => {
|
|
211
212
|
const [ok, error] = await tryFn(async () => {
|
|
212
213
|
await plugin.processReplicatorEvent('delete', resource.name, data.id, data);
|
|
213
214
|
});
|
|
214
|
-
|
|
215
|
+
|
|
215
216
|
if (!ok) {
|
|
216
217
|
if (this.config.verbose) {
|
|
217
218
|
console.warn(`[ReplicatorPlugin] Delete event failed for resource ${resource.name}: ${error.message}`);
|
|
218
219
|
}
|
|
219
220
|
this.emit('error', { operation: 'delete', error: error.message, resource: resource.name });
|
|
220
221
|
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Save handler references
|
|
225
|
+
this.eventHandlers.set(resource.name, {
|
|
226
|
+
insert: insertHandler,
|
|
227
|
+
update: updateHandler,
|
|
228
|
+
delete: deleteHandler
|
|
221
229
|
});
|
|
222
230
|
|
|
231
|
+
// Attach listeners
|
|
232
|
+
resource.on('insert', insertHandler);
|
|
233
|
+
resource.on('update', updateHandler);
|
|
234
|
+
resource.on('delete', deleteHandler);
|
|
235
|
+
|
|
223
236
|
this.eventListenersInstalled.add(resource.name);
|
|
224
237
|
}
|
|
225
238
|
|
|
@@ -229,7 +242,7 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
229
242
|
// Create replicator log resource if enabled
|
|
230
243
|
if (this.config.persistReplicatorLog) {
|
|
231
244
|
const [ok, err, logResource] = await tryFn(() => database.createResource({
|
|
232
|
-
name: this.config.replicatorLogResource || '
|
|
245
|
+
name: this.config.replicatorLogResource || 'plg_replicator_logs',
|
|
233
246
|
attributes: {
|
|
234
247
|
id: 'string|required',
|
|
235
248
|
resource: 'string|required',
|
|
@@ -244,7 +257,7 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
244
257
|
if (ok) {
|
|
245
258
|
this.replicatorLogResource = logResource;
|
|
246
259
|
} else {
|
|
247
|
-
this.replicatorLogResource = database.resources[this.config.replicatorLogResource || '
|
|
260
|
+
this.replicatorLogResource = database.resources[this.config.replicatorLogResource || 'plg_replicator_logs'];
|
|
248
261
|
}
|
|
249
262
|
}
|
|
250
263
|
|
|
@@ -256,7 +269,7 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
256
269
|
|
|
257
270
|
// Install event listeners for existing resources
|
|
258
271
|
for (const resource of Object.values(database.resources)) {
|
|
259
|
-
if (resource.name !== (this.config.replicatorLogResource || '
|
|
272
|
+
if (resource.name !== (this.config.replicatorLogResource || 'plg_replicator_logs')) {
|
|
260
273
|
this.installEventListeners(resource, database, this);
|
|
261
274
|
}
|
|
262
275
|
}
|
|
@@ -279,17 +292,22 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
279
292
|
}
|
|
280
293
|
|
|
281
294
|
installDatabaseHooks() {
|
|
282
|
-
//
|
|
283
|
-
this.
|
|
284
|
-
if (resource.name !== (this.config.replicatorLogResource || '
|
|
295
|
+
// Store hook reference for later removal
|
|
296
|
+
this._afterCreateResourceHook = (resource) => {
|
|
297
|
+
if (resource.name !== (this.config.replicatorLogResource || 'plg_replicator_logs')) {
|
|
285
298
|
this.installEventListeners(resource, this.database, this);
|
|
286
299
|
}
|
|
287
|
-
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
this.database.addHook('afterCreateResource', this._afterCreateResourceHook);
|
|
288
303
|
}
|
|
289
304
|
|
|
290
305
|
removeDatabaseHooks() {
|
|
291
|
-
// Remove the hook we added
|
|
292
|
-
|
|
306
|
+
// Remove the hook we added using stored reference
|
|
307
|
+
if (this._afterCreateResourceHook) {
|
|
308
|
+
this.database.removeHook('afterCreateResource', this._afterCreateResourceHook);
|
|
309
|
+
this._afterCreateResourceHook = null;
|
|
310
|
+
}
|
|
293
311
|
}
|
|
294
312
|
|
|
295
313
|
createReplicator(driver, config, resources, client) {
|
|
@@ -324,16 +342,16 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
324
342
|
async retryWithBackoff(operation, maxRetries = 3) {
|
|
325
343
|
let lastError;
|
|
326
344
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
327
|
-
const [ok, error] = await tryFn(operation);
|
|
328
|
-
|
|
345
|
+
const [ok, error, result] = await tryFn(operation);
|
|
346
|
+
|
|
329
347
|
if (ok) {
|
|
330
|
-
return
|
|
348
|
+
return result;
|
|
331
349
|
} else {
|
|
332
350
|
lastError = error;
|
|
333
351
|
if (this.config.verbose) {
|
|
334
352
|
console.warn(`[ReplicatorPlugin] Retry attempt ${attempt}/${maxRetries} failed: ${error.message}`);
|
|
335
353
|
}
|
|
336
|
-
|
|
354
|
+
|
|
337
355
|
if (attempt === maxRetries) {
|
|
338
356
|
throw error;
|
|
339
357
|
}
|
|
@@ -438,7 +456,7 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
438
456
|
return Promise.allSettled(promises);
|
|
439
457
|
}
|
|
440
458
|
|
|
441
|
-
async
|
|
459
|
+
async processReplicatorItem(item) {
|
|
442
460
|
const applicableReplicators = this.replicators.filter(replicator => {
|
|
443
461
|
const should = replicator.shouldReplicateResource && replicator.shouldReplicateResource(item.resourceName, item.operation);
|
|
444
462
|
return should;
|
|
@@ -512,14 +530,10 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
512
530
|
return Promise.allSettled(promises);
|
|
513
531
|
}
|
|
514
532
|
|
|
515
|
-
async
|
|
516
|
-
|
|
533
|
+
async logReplicator(item) {
|
|
534
|
+
// Always use the saved reference
|
|
517
535
|
const logRes = this.replicatorLog || this.database.resources[normalizeResourceName(this.config.replicatorLogResource)];
|
|
518
536
|
if (!logRes) {
|
|
519
|
-
if (this.database) {
|
|
520
|
-
if (this.database.options && this.database.options.connectionString) {
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
537
|
this.emit('replicator.log.failed', { error: 'replicator log resource not found', item });
|
|
524
538
|
return;
|
|
525
539
|
}
|
|
@@ -544,7 +558,7 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
544
558
|
}
|
|
545
559
|
}
|
|
546
560
|
|
|
547
|
-
async
|
|
561
|
+
async updateReplicatorLog(logId, updates) {
|
|
548
562
|
if (!this.replicatorLog) return;
|
|
549
563
|
|
|
550
564
|
const [ok, err] = await tryFn(async () => {
|
|
@@ -559,7 +573,7 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
559
573
|
}
|
|
560
574
|
|
|
561
575
|
// Utility methods
|
|
562
|
-
async
|
|
576
|
+
async getReplicatorStats() {
|
|
563
577
|
const replicatorStats = await Promise.all(
|
|
564
578
|
this.replicators.map(async (replicator) => {
|
|
565
579
|
const status = await replicator.getStatus();
|
|
@@ -574,16 +588,12 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
574
588
|
|
|
575
589
|
return {
|
|
576
590
|
replicators: replicatorStats,
|
|
577
|
-
queue: {
|
|
578
|
-
length: this.queue.length,
|
|
579
|
-
isProcessing: this.isProcessing
|
|
580
|
-
},
|
|
581
591
|
stats: this.stats,
|
|
582
592
|
lastSync: this.stats.lastSync
|
|
583
593
|
};
|
|
584
594
|
}
|
|
585
595
|
|
|
586
|
-
async
|
|
596
|
+
async getReplicatorLogs(options = {}) {
|
|
587
597
|
if (!this.replicatorLog) {
|
|
588
598
|
return [];
|
|
589
599
|
}
|
|
@@ -596,43 +606,42 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
596
606
|
offset = 0
|
|
597
607
|
} = options;
|
|
598
608
|
|
|
599
|
-
|
|
600
|
-
|
|
609
|
+
const filter = {};
|
|
610
|
+
|
|
601
611
|
if (resourceName) {
|
|
602
|
-
|
|
612
|
+
filter.resourceName = resourceName;
|
|
603
613
|
}
|
|
604
|
-
|
|
614
|
+
|
|
605
615
|
if (operation) {
|
|
606
|
-
|
|
616
|
+
filter.operation = operation;
|
|
607
617
|
}
|
|
608
|
-
|
|
618
|
+
|
|
609
619
|
if (status) {
|
|
610
|
-
|
|
620
|
+
filter.status = status;
|
|
611
621
|
}
|
|
612
622
|
|
|
613
|
-
const logs = await this.replicatorLog.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
return logs.slice(offset, offset + limit);
|
|
623
|
+
const logs = await this.replicatorLog.query(filter, { limit, offset });
|
|
624
|
+
|
|
625
|
+
return logs || [];
|
|
617
626
|
}
|
|
618
627
|
|
|
619
|
-
async
|
|
628
|
+
async retryFailedReplicators() {
|
|
620
629
|
if (!this.replicatorLog) {
|
|
621
630
|
return { retried: 0 };
|
|
622
631
|
}
|
|
623
632
|
|
|
624
|
-
const failedLogs = await this.replicatorLog.
|
|
633
|
+
const failedLogs = await this.replicatorLog.query({
|
|
625
634
|
status: 'failed'
|
|
626
635
|
});
|
|
627
636
|
|
|
628
637
|
let retried = 0;
|
|
629
|
-
|
|
630
|
-
for (const log of failedLogs) {
|
|
638
|
+
|
|
639
|
+
for (const log of failedLogs || []) {
|
|
631
640
|
const [ok, err] = await tryFn(async () => {
|
|
632
641
|
// Re-queue the replicator
|
|
633
642
|
await this.processReplicatorEvent(
|
|
634
|
-
log.resourceName,
|
|
635
643
|
log.operation,
|
|
644
|
+
log.resourceName,
|
|
636
645
|
log.recordId,
|
|
637
646
|
log.data
|
|
638
647
|
);
|
|
@@ -656,16 +665,30 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
656
665
|
this.stats.lastSync = new Date().toISOString();
|
|
657
666
|
|
|
658
667
|
for (const resourceName in this.database.resources) {
|
|
659
|
-
if (normalizeResourceName(resourceName) === normalizeResourceName('
|
|
668
|
+
if (normalizeResourceName(resourceName) === normalizeResourceName('plg_replicator_logs')) continue;
|
|
660
669
|
|
|
661
670
|
if (replicator.shouldReplicateResource(resourceName)) {
|
|
662
671
|
this.emit('replicator.sync.resource', { resourceName, replicatorId });
|
|
663
|
-
|
|
672
|
+
|
|
664
673
|
const resource = this.database.resources[resourceName];
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
674
|
+
|
|
675
|
+
// Use pagination to avoid memory issues
|
|
676
|
+
let offset = 0;
|
|
677
|
+
const pageSize = this.config.batchSize || 100;
|
|
678
|
+
|
|
679
|
+
while (true) {
|
|
680
|
+
const [ok, err, page] = await tryFn(() => resource.page({ offset, size: pageSize }));
|
|
681
|
+
|
|
682
|
+
if (!ok || !page) break;
|
|
683
|
+
|
|
684
|
+
const records = Array.isArray(page) ? page : (page.items || []);
|
|
685
|
+
if (records.length === 0) break;
|
|
686
|
+
|
|
687
|
+
for (const record of records) {
|
|
688
|
+
await replicator.replicate(resourceName, 'insert', record, record.id);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
offset += pageSize;
|
|
669
692
|
}
|
|
670
693
|
}
|
|
671
694
|
}
|
|
@@ -698,10 +721,25 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
698
721
|
await Promise.allSettled(cleanupPromises);
|
|
699
722
|
}
|
|
700
723
|
|
|
724
|
+
// Remove event listeners from resources to prevent memory leaks
|
|
725
|
+
if (this.database && this.database.resources) {
|
|
726
|
+
for (const resourceName of this.eventListenersInstalled) {
|
|
727
|
+
const resource = this.database.resources[resourceName];
|
|
728
|
+
const handlers = this.eventHandlers.get(resourceName);
|
|
729
|
+
|
|
730
|
+
if (resource && handlers) {
|
|
731
|
+
resource.off('insert', handlers.insert);
|
|
732
|
+
resource.off('update', handlers.update);
|
|
733
|
+
resource.off('delete', handlers.delete);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
701
738
|
this.replicators = [];
|
|
702
739
|
this.database = null;
|
|
703
740
|
this.eventListenersInstalled.clear();
|
|
704
|
-
|
|
741
|
+
this.eventHandlers.clear();
|
|
742
|
+
|
|
705
743
|
this.removeAllListeners();
|
|
706
744
|
});
|
|
707
745
|
|
|
@@ -328,17 +328,21 @@ class S3dbReplicator extends BaseReplicator {
|
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
_getDestResourceObj(resource) {
|
|
331
|
-
const
|
|
331
|
+
const db = this.targetDatabase || this.client;
|
|
332
|
+
const available = Object.keys(db.resources || {});
|
|
332
333
|
const norm = normalizeResourceName(resource);
|
|
333
334
|
const found = available.find(r => normalizeResourceName(r) === norm);
|
|
334
335
|
if (!found) {
|
|
335
336
|
throw new Error(`[S3dbReplicator] Destination resource not found: ${resource}. Available: ${available.join(', ')}`);
|
|
336
337
|
}
|
|
337
|
-
return
|
|
338
|
+
return db.resources[found];
|
|
338
339
|
}
|
|
339
340
|
|
|
340
341
|
async replicateBatch(resourceName, records) {
|
|
341
|
-
if (
|
|
342
|
+
if (this.enabled === false) {
|
|
343
|
+
return { skipped: true, reason: 'replicator_disabled' };
|
|
344
|
+
}
|
|
345
|
+
if (!this.shouldReplicateResource(resourceName)) {
|
|
342
346
|
return { skipped: true, reason: 'resource_not_included' };
|
|
343
347
|
}
|
|
344
348
|
|
|
@@ -32,11 +32,13 @@ class SqsReplicator extends BaseReplicator {
|
|
|
32
32
|
this.client = client;
|
|
33
33
|
this.queueUrl = config.queueUrl;
|
|
34
34
|
this.queues = config.queues || {};
|
|
35
|
-
|
|
35
|
+
// Support legacy names but prefer defaultQueue
|
|
36
|
+
this.defaultQueue = config.defaultQueue || config.defaultQueueUrl || config.queueUrlDefault || null;
|
|
36
37
|
this.region = config.region || 'us-east-1';
|
|
37
38
|
this.sqsClient = client || null;
|
|
38
39
|
this.messageGroupId = config.messageGroupId;
|
|
39
40
|
this.deduplicationId = config.deduplicationId;
|
|
41
|
+
this.resourceQueueMap = config.resourceQueueMap || null;
|
|
40
42
|
|
|
41
43
|
// Normalize resources to object format
|
|
42
44
|
if (Array.isArray(resources)) {
|
|
@@ -188,7 +190,10 @@ class SqsReplicator extends BaseReplicator {
|
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
async replicate(resource, operation, data, id, beforeData = null) {
|
|
191
|
-
if (
|
|
193
|
+
if (this.enabled === false) {
|
|
194
|
+
return { skipped: true, reason: 'replicator_disabled' };
|
|
195
|
+
}
|
|
196
|
+
if (!this.shouldReplicateResource(resource)) {
|
|
192
197
|
return { skipped: true, reason: 'resource_not_included' };
|
|
193
198
|
}
|
|
194
199
|
const [ok, err, result] = await tryFn(async () => {
|
|
@@ -234,7 +239,10 @@ class SqsReplicator extends BaseReplicator {
|
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
async replicateBatch(resource, records) {
|
|
237
|
-
if (
|
|
242
|
+
if (this.enabled === false) {
|
|
243
|
+
return { skipped: true, reason: 'replicator_disabled' };
|
|
244
|
+
}
|
|
245
|
+
if (!this.shouldReplicateResource(resource)) {
|
|
238
246
|
return { skipped: true, reason: 'resource_not_included' };
|
|
239
247
|
}
|
|
240
248
|
const [ok, err, result] = await tryFn(async () => {
|