s3db.js 10.0.16 → 10.0.17
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/dist/s3db.cjs.js +1758 -1549
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1758 -1549
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/plugins/eventual-consistency/analytics.js +668 -0
- package/src/plugins/eventual-consistency/config.js +120 -0
- package/src/plugins/eventual-consistency/consolidation.js +770 -0
- package/src/plugins/eventual-consistency/garbage-collection.js +126 -0
- package/src/plugins/eventual-consistency/helpers.js +179 -0
- package/src/plugins/eventual-consistency/index.js +455 -0
- package/src/plugins/eventual-consistency/locks.js +77 -0
- package/src/plugins/eventual-consistency/partitions.js +45 -0
- package/src/plugins/eventual-consistency/setup.js +298 -0
- package/src/plugins/eventual-consistency/transactions.js +119 -0
- package/src/plugins/eventual-consistency/utils.js +182 -0
- package/src/plugins/eventual-consistency.plugin.js +195 -2
- package/src/plugins/index.js +1 -1
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventualConsistencyPlugin - Main export
|
|
3
|
+
* Provides eventually consistent counters using transaction log pattern
|
|
4
|
+
* @module eventual-consistency
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Plugin from "../plugin.class.js";
|
|
8
|
+
import { createConfig, validateResourcesConfig, logConfigWarnings, logInitialization } from "./config.js";
|
|
9
|
+
import { detectTimezone, getCohortInfo, createFieldHandler } from "./utils.js";
|
|
10
|
+
import { createPartitionConfig } from "./partitions.js";
|
|
11
|
+
import { createTransaction } from "./transactions.js";
|
|
12
|
+
import {
|
|
13
|
+
consolidateRecord,
|
|
14
|
+
getConsolidatedValue,
|
|
15
|
+
getCohortStats,
|
|
16
|
+
recalculateRecord,
|
|
17
|
+
runConsolidation
|
|
18
|
+
} from "./consolidation.js";
|
|
19
|
+
import { runGarbageCollection } from "./garbage-collection.js";
|
|
20
|
+
import { updateAnalytics, getAnalytics, getMonthByDay, getDayByHour, getLastNDays, getYearByMonth, getMonthByHour, getTopRecords } from "./analytics.js";
|
|
21
|
+
import { onSetup, onStart, onStop, watchForResource, completeFieldSetup } from "./setup.js";
|
|
22
|
+
|
|
23
|
+
export class EventualConsistencyPlugin extends Plugin {
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
super(options);
|
|
26
|
+
|
|
27
|
+
// Validate resources structure
|
|
28
|
+
validateResourcesConfig(options.resources);
|
|
29
|
+
|
|
30
|
+
// Auto-detect timezone
|
|
31
|
+
const detectedTimezone = detectTimezone();
|
|
32
|
+
const timezoneAutoDetected = !options.cohort?.timezone;
|
|
33
|
+
|
|
34
|
+
// Create shared configuration
|
|
35
|
+
this.config = createConfig(options, detectedTimezone);
|
|
36
|
+
|
|
37
|
+
// Create field handlers map
|
|
38
|
+
this.fieldHandlers = new Map(); // Map<resourceName, Map<fieldName, handler>>
|
|
39
|
+
|
|
40
|
+
// Parse resources configuration
|
|
41
|
+
for (const [resourceName, fields] of Object.entries(options.resources)) {
|
|
42
|
+
const resourceHandlers = new Map();
|
|
43
|
+
for (const fieldName of fields) {
|
|
44
|
+
// Create a field handler for each resource/field combination
|
|
45
|
+
resourceHandlers.set(fieldName, createFieldHandler(resourceName, fieldName));
|
|
46
|
+
}
|
|
47
|
+
this.fieldHandlers.set(resourceName, resourceHandlers);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Log warnings and initialization
|
|
51
|
+
logConfigWarnings(this.config);
|
|
52
|
+
logInitialization(this.config, this.fieldHandlers, timezoneAutoDetected);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Setup hook - create resources and register helpers
|
|
57
|
+
*/
|
|
58
|
+
async onSetup() {
|
|
59
|
+
await onSetup(
|
|
60
|
+
this.database,
|
|
61
|
+
this.fieldHandlers,
|
|
62
|
+
(handler) => completeFieldSetup(handler, this.database, this.config, this),
|
|
63
|
+
(resourceName) => watchForResource(resourceName, this.database, this.fieldHandlers,
|
|
64
|
+
(handler) => completeFieldSetup(handler, this.database, this.config, this))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Start hook - begin timers and emit events
|
|
70
|
+
*/
|
|
71
|
+
async onStart() {
|
|
72
|
+
await onStart(
|
|
73
|
+
this.fieldHandlers,
|
|
74
|
+
this.config,
|
|
75
|
+
(handler, resourceName, fieldName) => this._runConsolidationForHandler(handler, resourceName, fieldName),
|
|
76
|
+
(handler, resourceName, fieldName) => this._runGarbageCollectionForHandler(handler, resourceName, fieldName),
|
|
77
|
+
(event, data) => this.emit(event, data)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stop hook - stop timers and flush pending
|
|
83
|
+
*/
|
|
84
|
+
async onStop() {
|
|
85
|
+
await onStop(
|
|
86
|
+
this.fieldHandlers,
|
|
87
|
+
(event, data) => this.emit(event, data)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create partition configuration
|
|
93
|
+
* @returns {Object} Partition configuration
|
|
94
|
+
*/
|
|
95
|
+
createPartitionConfig() {
|
|
96
|
+
return createPartitionConfig();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get cohort information for a date
|
|
101
|
+
* @param {Date} date - Date to get cohort info for
|
|
102
|
+
* @returns {Object} Cohort information
|
|
103
|
+
*/
|
|
104
|
+
getCohortInfo(date) {
|
|
105
|
+
return getCohortInfo(date, this.config.cohort.timezone, this.config.verbose);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create a transaction for a field handler
|
|
110
|
+
* @param {Object} handler - Field handler
|
|
111
|
+
* @param {Object} data - Transaction data
|
|
112
|
+
* @returns {Promise<Object|null>} Created transaction
|
|
113
|
+
*/
|
|
114
|
+
async createTransaction(handler, data) {
|
|
115
|
+
return await createTransaction(handler, data, this.config);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Consolidate a single record (internal method)
|
|
120
|
+
* This is used internally by consolidation timers and helper methods
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
123
|
+
async consolidateRecord(originalId) {
|
|
124
|
+
return await consolidateRecord(
|
|
125
|
+
originalId,
|
|
126
|
+
this.transactionResource,
|
|
127
|
+
this.targetResource,
|
|
128
|
+
this.lockResource,
|
|
129
|
+
this.analyticsResource,
|
|
130
|
+
(transactions) => this.updateAnalytics(transactions),
|
|
131
|
+
this.config
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get consolidated value without applying (internal method)
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
async getConsolidatedValue(originalId, options = {}) {
|
|
140
|
+
return await getConsolidatedValue(
|
|
141
|
+
originalId,
|
|
142
|
+
options,
|
|
143
|
+
this.transactionResource,
|
|
144
|
+
this.targetResource,
|
|
145
|
+
this.config
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get cohort statistics
|
|
151
|
+
* @param {string} cohortDate - Cohort date
|
|
152
|
+
* @returns {Promise<Object|null>} Cohort statistics
|
|
153
|
+
*/
|
|
154
|
+
async getCohortStats(cohortDate) {
|
|
155
|
+
return await getCohortStats(cohortDate, this.transactionResource);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Recalculate from scratch (internal method)
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
async recalculateRecord(originalId) {
|
|
163
|
+
return await recalculateRecord(
|
|
164
|
+
originalId,
|
|
165
|
+
this.transactionResource,
|
|
166
|
+
this.targetResource,
|
|
167
|
+
this.lockResource,
|
|
168
|
+
(id) => this.consolidateRecord(id),
|
|
169
|
+
this.config
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Update analytics
|
|
175
|
+
* @private
|
|
176
|
+
*/
|
|
177
|
+
async updateAnalytics(transactions) {
|
|
178
|
+
return await updateAnalytics(transactions, this.analyticsResource, this.config);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Helper method for sync mode consolidation
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
async _syncModeConsolidate(handler, id, field) {
|
|
186
|
+
// Temporarily set config for legacy methods
|
|
187
|
+
const oldResource = this.config.resource;
|
|
188
|
+
const oldField = this.config.field;
|
|
189
|
+
const oldTransactionResource = this.transactionResource;
|
|
190
|
+
const oldTargetResource = this.targetResource;
|
|
191
|
+
const oldLockResource = this.lockResource;
|
|
192
|
+
const oldAnalyticsResource = this.analyticsResource;
|
|
193
|
+
|
|
194
|
+
this.config.resource = handler.resource;
|
|
195
|
+
this.config.field = handler.field;
|
|
196
|
+
this.transactionResource = handler.transactionResource;
|
|
197
|
+
this.targetResource = handler.targetResource;
|
|
198
|
+
this.lockResource = handler.lockResource;
|
|
199
|
+
this.analyticsResource = handler.analyticsResource;
|
|
200
|
+
|
|
201
|
+
const result = await this.consolidateRecord(id);
|
|
202
|
+
|
|
203
|
+
// Restore
|
|
204
|
+
this.config.resource = oldResource;
|
|
205
|
+
this.config.field = oldField;
|
|
206
|
+
this.transactionResource = oldTransactionResource;
|
|
207
|
+
this.targetResource = oldTargetResource;
|
|
208
|
+
this.lockResource = oldLockResource;
|
|
209
|
+
this.analyticsResource = oldAnalyticsResource;
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Helper method for consolidate with handler
|
|
216
|
+
* @private
|
|
217
|
+
*/
|
|
218
|
+
async _consolidateWithHandler(handler, id) {
|
|
219
|
+
const oldResource = this.config.resource;
|
|
220
|
+
const oldField = this.config.field;
|
|
221
|
+
const oldTransactionResource = this.transactionResource;
|
|
222
|
+
const oldTargetResource = this.targetResource;
|
|
223
|
+
const oldLockResource = this.lockResource;
|
|
224
|
+
const oldAnalyticsResource = this.analyticsResource;
|
|
225
|
+
|
|
226
|
+
this.config.resource = handler.resource;
|
|
227
|
+
this.config.field = handler.field;
|
|
228
|
+
this.transactionResource = handler.transactionResource;
|
|
229
|
+
this.targetResource = handler.targetResource;
|
|
230
|
+
this.lockResource = handler.lockResource;
|
|
231
|
+
this.analyticsResource = handler.analyticsResource;
|
|
232
|
+
|
|
233
|
+
const result = await this.consolidateRecord(id);
|
|
234
|
+
|
|
235
|
+
this.config.resource = oldResource;
|
|
236
|
+
this.config.field = oldField;
|
|
237
|
+
this.transactionResource = oldTransactionResource;
|
|
238
|
+
this.targetResource = oldTargetResource;
|
|
239
|
+
this.lockResource = oldLockResource;
|
|
240
|
+
this.analyticsResource = oldAnalyticsResource;
|
|
241
|
+
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Helper method for getConsolidatedValue with handler
|
|
247
|
+
* @private
|
|
248
|
+
*/
|
|
249
|
+
async _getConsolidatedValueWithHandler(handler, id, options) {
|
|
250
|
+
const oldResource = this.config.resource;
|
|
251
|
+
const oldField = this.config.field;
|
|
252
|
+
const oldTransactionResource = this.transactionResource;
|
|
253
|
+
const oldTargetResource = this.targetResource;
|
|
254
|
+
|
|
255
|
+
this.config.resource = handler.resource;
|
|
256
|
+
this.config.field = handler.field;
|
|
257
|
+
this.transactionResource = handler.transactionResource;
|
|
258
|
+
this.targetResource = handler.targetResource;
|
|
259
|
+
|
|
260
|
+
const result = await this.getConsolidatedValue(id, options);
|
|
261
|
+
|
|
262
|
+
this.config.resource = oldResource;
|
|
263
|
+
this.config.field = oldField;
|
|
264
|
+
this.transactionResource = oldTransactionResource;
|
|
265
|
+
this.targetResource = oldTargetResource;
|
|
266
|
+
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Helper method for recalculate with handler
|
|
272
|
+
* @private
|
|
273
|
+
*/
|
|
274
|
+
async _recalculateWithHandler(handler, id) {
|
|
275
|
+
const oldResource = this.config.resource;
|
|
276
|
+
const oldField = this.config.field;
|
|
277
|
+
const oldTransactionResource = this.transactionResource;
|
|
278
|
+
const oldTargetResource = this.targetResource;
|
|
279
|
+
const oldLockResource = this.lockResource;
|
|
280
|
+
const oldAnalyticsResource = this.analyticsResource;
|
|
281
|
+
|
|
282
|
+
this.config.resource = handler.resource;
|
|
283
|
+
this.config.field = handler.field;
|
|
284
|
+
this.transactionResource = handler.transactionResource;
|
|
285
|
+
this.targetResource = handler.targetResource;
|
|
286
|
+
this.lockResource = handler.lockResource;
|
|
287
|
+
this.analyticsResource = handler.analyticsResource;
|
|
288
|
+
|
|
289
|
+
const result = await this.recalculateRecord(id);
|
|
290
|
+
|
|
291
|
+
this.config.resource = oldResource;
|
|
292
|
+
this.config.field = oldField;
|
|
293
|
+
this.transactionResource = oldTransactionResource;
|
|
294
|
+
this.targetResource = oldTargetResource;
|
|
295
|
+
this.lockResource = oldLockResource;
|
|
296
|
+
this.analyticsResource = oldAnalyticsResource;
|
|
297
|
+
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Run consolidation for a handler
|
|
303
|
+
* @private
|
|
304
|
+
*/
|
|
305
|
+
async _runConsolidationForHandler(handler, resourceName, fieldName) {
|
|
306
|
+
const oldResource = this.config.resource;
|
|
307
|
+
const oldField = this.config.field;
|
|
308
|
+
const oldTransactionResource = this.transactionResource;
|
|
309
|
+
const oldTargetResource = this.targetResource;
|
|
310
|
+
const oldLockResource = this.lockResource;
|
|
311
|
+
const oldAnalyticsResource = this.analyticsResource;
|
|
312
|
+
|
|
313
|
+
this.config.resource = resourceName;
|
|
314
|
+
this.config.field = fieldName;
|
|
315
|
+
this.transactionResource = handler.transactionResource;
|
|
316
|
+
this.targetResource = handler.targetResource;
|
|
317
|
+
this.lockResource = handler.lockResource;
|
|
318
|
+
this.analyticsResource = handler.analyticsResource;
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
await runConsolidation(
|
|
322
|
+
this.transactionResource,
|
|
323
|
+
(id) => this.consolidateRecord(id),
|
|
324
|
+
(event, data) => this.emit(event, data),
|
|
325
|
+
this.config
|
|
326
|
+
);
|
|
327
|
+
} finally {
|
|
328
|
+
this.config.resource = oldResource;
|
|
329
|
+
this.config.field = oldField;
|
|
330
|
+
this.transactionResource = oldTransactionResource;
|
|
331
|
+
this.targetResource = oldTargetResource;
|
|
332
|
+
this.lockResource = oldLockResource;
|
|
333
|
+
this.analyticsResource = oldAnalyticsResource;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Run garbage collection for a handler
|
|
339
|
+
* @private
|
|
340
|
+
*/
|
|
341
|
+
async _runGarbageCollectionForHandler(handler, resourceName, fieldName) {
|
|
342
|
+
const oldResource = this.config.resource;
|
|
343
|
+
const oldField = this.config.field;
|
|
344
|
+
const oldTransactionResource = this.transactionResource;
|
|
345
|
+
const oldTargetResource = this.targetResource;
|
|
346
|
+
const oldLockResource = this.lockResource;
|
|
347
|
+
|
|
348
|
+
this.config.resource = resourceName;
|
|
349
|
+
this.config.field = fieldName;
|
|
350
|
+
this.transactionResource = handler.transactionResource;
|
|
351
|
+
this.targetResource = handler.targetResource;
|
|
352
|
+
this.lockResource = handler.lockResource;
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
await runGarbageCollection(
|
|
356
|
+
this.transactionResource,
|
|
357
|
+
this.lockResource,
|
|
358
|
+
this.config,
|
|
359
|
+
(event, data) => this.emit(event, data)
|
|
360
|
+
);
|
|
361
|
+
} finally {
|
|
362
|
+
this.config.resource = oldResource;
|
|
363
|
+
this.config.field = oldField;
|
|
364
|
+
this.transactionResource = oldTransactionResource;
|
|
365
|
+
this.targetResource = oldTargetResource;
|
|
366
|
+
this.lockResource = oldLockResource;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Public Analytics API
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get analytics for a specific period
|
|
374
|
+
* @param {string} resourceName - Resource name
|
|
375
|
+
* @param {string} field - Field name
|
|
376
|
+
* @param {Object} options - Query options
|
|
377
|
+
* @returns {Promise<Array>} Analytics data
|
|
378
|
+
*/
|
|
379
|
+
async getAnalytics(resourceName, field, options = {}) {
|
|
380
|
+
return await getAnalytics(resourceName, field, options, this.fieldHandlers);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get analytics for entire month, broken down by days
|
|
385
|
+
* @param {string} resourceName - Resource name
|
|
386
|
+
* @param {string} field - Field name
|
|
387
|
+
* @param {string} month - Month in YYYY-MM format
|
|
388
|
+
* @param {Object} options - Options
|
|
389
|
+
* @returns {Promise<Array>} Daily analytics for the month
|
|
390
|
+
*/
|
|
391
|
+
async getMonthByDay(resourceName, field, month, options = {}) {
|
|
392
|
+
return await getMonthByDay(resourceName, field, month, options, this.fieldHandlers);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get analytics for entire day, broken down by hours
|
|
397
|
+
* @param {string} resourceName - Resource name
|
|
398
|
+
* @param {string} field - Field name
|
|
399
|
+
* @param {string} date - Date in YYYY-MM-DD format
|
|
400
|
+
* @param {Object} options - Options
|
|
401
|
+
* @returns {Promise<Array>} Hourly analytics for the day
|
|
402
|
+
*/
|
|
403
|
+
async getDayByHour(resourceName, field, date, options = {}) {
|
|
404
|
+
return await getDayByHour(resourceName, field, date, options, this.fieldHandlers);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Get analytics for last N days, broken down by days
|
|
409
|
+
* @param {string} resourceName - Resource name
|
|
410
|
+
* @param {string} field - Field name
|
|
411
|
+
* @param {number} days - Number of days to look back (default: 7)
|
|
412
|
+
* @param {Object} options - Options
|
|
413
|
+
* @returns {Promise<Array>} Daily analytics
|
|
414
|
+
*/
|
|
415
|
+
async getLastNDays(resourceName, field, days = 7, options = {}) {
|
|
416
|
+
return await getLastNDays(resourceName, field, days, options, this.fieldHandlers);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Get analytics for entire year, broken down by months
|
|
421
|
+
* @param {string} resourceName - Resource name
|
|
422
|
+
* @param {string} field - Field name
|
|
423
|
+
* @param {number} year - Year (e.g., 2025)
|
|
424
|
+
* @param {Object} options - Options
|
|
425
|
+
* @returns {Promise<Array>} Monthly analytics for the year
|
|
426
|
+
*/
|
|
427
|
+
async getYearByMonth(resourceName, field, year, options = {}) {
|
|
428
|
+
return await getYearByMonth(resourceName, field, year, options, this.fieldHandlers);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Get analytics for entire month, broken down by hours
|
|
433
|
+
* @param {string} resourceName - Resource name
|
|
434
|
+
* @param {string} field - Field name
|
|
435
|
+
* @param {string} month - Month in YYYY-MM format (or 'last' for previous month)
|
|
436
|
+
* @param {Object} options - Options
|
|
437
|
+
* @returns {Promise<Array>} Hourly analytics for the month
|
|
438
|
+
*/
|
|
439
|
+
async getMonthByHour(resourceName, field, month, options = {}) {
|
|
440
|
+
return await getMonthByHour(resourceName, field, month, options, this.fieldHandlers);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Get top records by volume
|
|
445
|
+
* @param {string} resourceName - Resource name
|
|
446
|
+
* @param {string} field - Field name
|
|
447
|
+
* @param {Object} options - Query options
|
|
448
|
+
* @returns {Promise<Array>} Top records
|
|
449
|
+
*/
|
|
450
|
+
async getTopRecords(resourceName, field, options = {}) {
|
|
451
|
+
return await getTopRecords(resourceName, field, options, this.fieldHandlers);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export default EventualConsistencyPlugin;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distributed locking for EventualConsistencyPlugin
|
|
3
|
+
* @module eventual-consistency/locks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import tryFn from "../../concerns/try-fn.js";
|
|
7
|
+
import { PromisePool } from "@supercharge/promise-pool";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Clean up stale locks that exceed the configured timeout
|
|
11
|
+
* Uses distributed locking to prevent multiple containers from cleaning simultaneously
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} lockResource - Lock resource instance
|
|
14
|
+
* @param {Object} config - Plugin configuration
|
|
15
|
+
* @returns {Promise<void>}
|
|
16
|
+
*/
|
|
17
|
+
export async function cleanupStaleLocks(lockResource, config) {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
const lockTimeoutMs = config.lockTimeout * 1000; // Convert seconds to ms
|
|
20
|
+
const cutoffTime = now - lockTimeoutMs;
|
|
21
|
+
|
|
22
|
+
// Acquire distributed lock for cleanup operation
|
|
23
|
+
const cleanupLockId = `lock-cleanup-${config.resource}-${config.field}`;
|
|
24
|
+
const [lockAcquired] = await tryFn(() =>
|
|
25
|
+
lockResource.insert({
|
|
26
|
+
id: cleanupLockId,
|
|
27
|
+
lockedAt: Date.now(),
|
|
28
|
+
workerId: process.pid ? String(process.pid) : 'unknown'
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// If another container is already cleaning, skip
|
|
33
|
+
if (!lockAcquired) {
|
|
34
|
+
if (config.verbose) {
|
|
35
|
+
console.log(`[EventualConsistency] Lock cleanup already running in another container`);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Get all locks
|
|
42
|
+
const [ok, err, locks] = await tryFn(() => lockResource.list());
|
|
43
|
+
|
|
44
|
+
if (!ok || !locks || locks.length === 0) return;
|
|
45
|
+
|
|
46
|
+
// Find stale locks (excluding the cleanup lock itself)
|
|
47
|
+
const staleLocks = locks.filter(lock =>
|
|
48
|
+
lock.id !== cleanupLockId && lock.lockedAt < cutoffTime
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (staleLocks.length === 0) return;
|
|
52
|
+
|
|
53
|
+
if (config.verbose) {
|
|
54
|
+
console.log(`[EventualConsistency] Cleaning up ${staleLocks.length} stale locks`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Delete stale locks using PromisePool
|
|
58
|
+
const { results, errors } = await PromisePool
|
|
59
|
+
.for(staleLocks)
|
|
60
|
+
.withConcurrency(5)
|
|
61
|
+
.process(async (lock) => {
|
|
62
|
+
const [deleted] = await tryFn(() => lockResource.delete(lock.id));
|
|
63
|
+
return deleted;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (errors && errors.length > 0 && config.verbose) {
|
|
67
|
+
console.warn(`[EventualConsistency] ${errors.length} stale locks failed to delete`);
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (config.verbose) {
|
|
71
|
+
console.warn(`[EventualConsistency] Error cleaning up stale locks:`, error.message);
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
// Always release cleanup lock
|
|
75
|
+
await tryFn(() => lockResource.delete(cleanupLockId));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partition configuration for EventualConsistencyPlugin
|
|
3
|
+
* @module eventual-consistency/partitions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create partition configuration for transaction resources
|
|
8
|
+
* This defines how transactions are organized in S3 for O(1) query performance
|
|
9
|
+
*
|
|
10
|
+
* @returns {Object} Partition configuration
|
|
11
|
+
*/
|
|
12
|
+
export function createPartitionConfig() {
|
|
13
|
+
// Create partitions for transactions
|
|
14
|
+
const partitions = {
|
|
15
|
+
// Composite partition by originalId + applied status
|
|
16
|
+
// This is THE MOST CRITICAL optimization for consolidation!
|
|
17
|
+
// Why: Consolidation always queries { originalId, applied: false }
|
|
18
|
+
// Without this: Reads ALL transactions (applied + pending) and filters manually
|
|
19
|
+
// With this: Reads ONLY pending transactions - can be 1000x faster!
|
|
20
|
+
byOriginalIdAndApplied: {
|
|
21
|
+
fields: {
|
|
22
|
+
originalId: 'string',
|
|
23
|
+
applied: 'boolean'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
// Partition by time cohorts for batch consolidation across many records
|
|
27
|
+
byHour: {
|
|
28
|
+
fields: {
|
|
29
|
+
cohortHour: 'string'
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
byDay: {
|
|
33
|
+
fields: {
|
|
34
|
+
cohortDate: 'string'
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
byMonth: {
|
|
38
|
+
fields: {
|
|
39
|
+
cohortMonth: 'string'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return partitions;
|
|
45
|
+
}
|