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
|
@@ -54,7 +54,14 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
54
54
|
metrics: options.analyticsConfig?.metrics || ['count', 'sum', 'avg', 'min', 'max'],
|
|
55
55
|
rollupStrategy: options.analyticsConfig?.rollupStrategy || 'incremental',
|
|
56
56
|
retentionDays: options.analyticsConfig?.retentionDays || 365
|
|
57
|
-
}
|
|
57
|
+
},
|
|
58
|
+
// Checkpoint configuration for high-volume scenarios
|
|
59
|
+
enableCheckpoints: options.enableCheckpoints !== false, // Default: true
|
|
60
|
+
checkpointStrategy: options.checkpointStrategy || 'hourly', // 'hourly', 'daily', 'manual', 'disabled'
|
|
61
|
+
checkpointRetention: options.checkpointRetention || 90, // Days to keep checkpoints
|
|
62
|
+
checkpointThreshold: options.checkpointThreshold || 1000, // Min transactions before creating checkpoint
|
|
63
|
+
deleteConsolidatedTransactions: options.deleteConsolidatedTransactions !== false, // Delete transactions after checkpoint
|
|
64
|
+
autoCheckpoint: options.autoCheckpoint !== false // Auto-create checkpoints for old cohorts
|
|
58
65
|
};
|
|
59
66
|
|
|
60
67
|
// Create field handlers map
|
|
@@ -116,6 +123,7 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
116
123
|
targetResource: null,
|
|
117
124
|
analyticsResource: null,
|
|
118
125
|
lockResource: null,
|
|
126
|
+
checkpointResource: null, // NEW: Checkpoint resource for high-volume optimization
|
|
119
127
|
consolidationTimer: null,
|
|
120
128
|
gcTimer: null,
|
|
121
129
|
pendingTransactions: new Map(),
|
|
@@ -371,8 +379,20 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
371
379
|
}
|
|
372
380
|
|
|
373
381
|
createPartitionConfig() {
|
|
374
|
-
// Create
|
|
382
|
+
// Create partitions for transactions
|
|
375
383
|
const partitions = {
|
|
384
|
+
// Composite partition by originalId + applied status
|
|
385
|
+
// This is THE MOST CRITICAL optimization for consolidation!
|
|
386
|
+
// Why: Consolidation always queries { originalId, applied: false }
|
|
387
|
+
// Without this: Reads ALL transactions (applied + pending) and filters manually
|
|
388
|
+
// With this: Reads ONLY pending transactions - can be 1000x faster!
|
|
389
|
+
byOriginalIdAndApplied: {
|
|
390
|
+
fields: {
|
|
391
|
+
originalId: 'string',
|
|
392
|
+
applied: 'boolean'
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
// Partition by time cohorts for batch consolidation across many records
|
|
376
396
|
byHour: {
|
|
377
397
|
fields: {
|
|
378
398
|
cohortHour: 'string'
|
|
@@ -730,6 +750,50 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
730
750
|
|
|
731
751
|
return result;
|
|
732
752
|
};
|
|
753
|
+
|
|
754
|
+
// Add method to recalculate from scratch
|
|
755
|
+
// Signature: recalculate(id, field)
|
|
756
|
+
resource.recalculate = async (id, field) => {
|
|
757
|
+
if (!field) {
|
|
758
|
+
throw new Error(`Field parameter is required: recalculate(id, field)`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const handler = resource._eventualConsistencyPlugins[field];
|
|
762
|
+
|
|
763
|
+
if (!handler) {
|
|
764
|
+
const availableFields = Object.keys(resource._eventualConsistencyPlugins).join(', ');
|
|
765
|
+
throw new Error(
|
|
766
|
+
`No eventual consistency plugin found for field "${field}". ` +
|
|
767
|
+
`Available fields: ${availableFields}`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Temporarily set config for legacy methods
|
|
772
|
+
const oldResource = plugin.config.resource;
|
|
773
|
+
const oldField = plugin.config.field;
|
|
774
|
+
const oldTransactionResource = plugin.transactionResource;
|
|
775
|
+
const oldTargetResource = plugin.targetResource;
|
|
776
|
+
const oldLockResource = plugin.lockResource;
|
|
777
|
+
const oldAnalyticsResource = plugin.analyticsResource;
|
|
778
|
+
|
|
779
|
+
plugin.config.resource = handler.resource;
|
|
780
|
+
plugin.config.field = handler.field;
|
|
781
|
+
plugin.transactionResource = handler.transactionResource;
|
|
782
|
+
plugin.targetResource = handler.targetResource;
|
|
783
|
+
plugin.lockResource = handler.lockResource;
|
|
784
|
+
plugin.analyticsResource = handler.analyticsResource;
|
|
785
|
+
|
|
786
|
+
const result = await plugin.recalculateRecord(id);
|
|
787
|
+
|
|
788
|
+
plugin.config.resource = oldResource;
|
|
789
|
+
plugin.config.field = oldField;
|
|
790
|
+
plugin.transactionResource = oldTransactionResource;
|
|
791
|
+
plugin.targetResource = oldTargetResource;
|
|
792
|
+
plugin.lockResource = oldLockResource;
|
|
793
|
+
plugin.analyticsResource = oldAnalyticsResource;
|
|
794
|
+
|
|
795
|
+
return result;
|
|
796
|
+
};
|
|
733
797
|
}
|
|
734
798
|
|
|
735
799
|
async createTransaction(handler, data) {
|
|
@@ -1493,6 +1557,135 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
1493
1557
|
return stats;
|
|
1494
1558
|
}
|
|
1495
1559
|
|
|
1560
|
+
/**
|
|
1561
|
+
* Recalculate from scratch by resetting all transactions to pending
|
|
1562
|
+
* This is useful for debugging, recovery, or when you want to recompute everything
|
|
1563
|
+
* @param {string} originalId - The ID of the record to recalculate
|
|
1564
|
+
* @returns {Promise<number>} The recalculated value
|
|
1565
|
+
*/
|
|
1566
|
+
async recalculateRecord(originalId) {
|
|
1567
|
+
// Clean up stale locks before attempting to acquire
|
|
1568
|
+
await this.cleanupStaleLocks();
|
|
1569
|
+
|
|
1570
|
+
// Acquire distributed lock to prevent concurrent operations
|
|
1571
|
+
const lockId = `lock-recalculate-${originalId}`;
|
|
1572
|
+
const [lockAcquired, lockErr, lock] = await tryFn(() =>
|
|
1573
|
+
this.lockResource.insert({
|
|
1574
|
+
id: lockId,
|
|
1575
|
+
lockedAt: Date.now(),
|
|
1576
|
+
workerId: process.pid ? String(process.pid) : 'unknown'
|
|
1577
|
+
})
|
|
1578
|
+
);
|
|
1579
|
+
|
|
1580
|
+
// If lock couldn't be acquired, another worker is operating on this record
|
|
1581
|
+
if (!lockAcquired) {
|
|
1582
|
+
if (this.config.verbose) {
|
|
1583
|
+
console.log(`[EventualConsistency] Recalculate lock for ${originalId} already held, skipping`);
|
|
1584
|
+
}
|
|
1585
|
+
throw new Error(`Cannot recalculate ${originalId}: lock already held by another worker`);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
try {
|
|
1589
|
+
if (this.config.verbose) {
|
|
1590
|
+
console.log(
|
|
1591
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
|
|
1592
|
+
`Starting recalculation for ${originalId} (resetting all transactions to pending)`
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// Get ALL transactions for this record (both applied and pending)
|
|
1597
|
+
const [allOk, allErr, allTransactions] = await tryFn(() =>
|
|
1598
|
+
this.transactionResource.query({
|
|
1599
|
+
originalId
|
|
1600
|
+
})
|
|
1601
|
+
);
|
|
1602
|
+
|
|
1603
|
+
if (!allOk || !allTransactions || allTransactions.length === 0) {
|
|
1604
|
+
if (this.config.verbose) {
|
|
1605
|
+
console.log(
|
|
1606
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
|
|
1607
|
+
`No transactions found for ${originalId}, nothing to recalculate`
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
return 0;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
if (this.config.verbose) {
|
|
1614
|
+
console.log(
|
|
1615
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
|
|
1616
|
+
`Found ${allTransactions.length} total transactions for ${originalId}, marking all as pending...`
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Mark ALL transactions as pending (applied: false)
|
|
1621
|
+
// Exclude anchor transactions (they should always be applied)
|
|
1622
|
+
const transactionsToReset = allTransactions.filter(txn => txn.source !== 'anchor');
|
|
1623
|
+
|
|
1624
|
+
const { results, errors } = await PromisePool
|
|
1625
|
+
.for(transactionsToReset)
|
|
1626
|
+
.withConcurrency(10)
|
|
1627
|
+
.process(async (txn) => {
|
|
1628
|
+
const [ok, err] = await tryFn(() =>
|
|
1629
|
+
this.transactionResource.update(txn.id, { applied: false })
|
|
1630
|
+
);
|
|
1631
|
+
|
|
1632
|
+
if (!ok && this.config.verbose) {
|
|
1633
|
+
console.warn(`[EventualConsistency] Failed to reset transaction ${txn.id}:`, err?.message);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
return ok;
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
if (errors && errors.length > 0) {
|
|
1640
|
+
console.warn(
|
|
1641
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
|
|
1642
|
+
`Failed to reset ${errors.length} transactions during recalculation`
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (this.config.verbose) {
|
|
1647
|
+
console.log(
|
|
1648
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
|
|
1649
|
+
`Reset ${results.length} transactions to pending, now resetting record value and running consolidation...`
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// Reset the record's field value to 0 to prevent double-counting
|
|
1654
|
+
// This ensures consolidation starts fresh without using the old value as an anchor
|
|
1655
|
+
const [resetOk, resetErr] = await tryFn(() =>
|
|
1656
|
+
this.targetResource.update(originalId, {
|
|
1657
|
+
[this.config.field]: 0
|
|
1658
|
+
})
|
|
1659
|
+
);
|
|
1660
|
+
|
|
1661
|
+
if (!resetOk && this.config.verbose) {
|
|
1662
|
+
console.warn(
|
|
1663
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
|
|
1664
|
+
`Failed to reset record value for ${originalId}: ${resetErr?.message}`
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// Now run normal consolidation which will process all pending transactions
|
|
1669
|
+
const consolidatedValue = await this.consolidateRecord(originalId);
|
|
1670
|
+
|
|
1671
|
+
if (this.config.verbose) {
|
|
1672
|
+
console.log(
|
|
1673
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
|
|
1674
|
+
`Recalculation complete for ${originalId}: final value = ${consolidatedValue}`
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
return consolidatedValue;
|
|
1679
|
+
} finally {
|
|
1680
|
+
// Always release the lock
|
|
1681
|
+
const [lockReleased, lockReleaseErr] = await tryFn(() => this.lockResource.delete(lockId));
|
|
1682
|
+
|
|
1683
|
+
if (!lockReleased && this.config.verbose) {
|
|
1684
|
+
console.warn(`[EventualConsistency] Failed to release recalculate lock ${lockId}:`, lockReleaseErr?.message);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1496
1689
|
/**
|
|
1497
1690
|
* Clean up stale locks that exceed the configured timeout
|
|
1498
1691
|
* Uses distributed locking to prevent multiple containers from cleaning simultaneously
|
package/src/plugins/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export * from './audit.plugin.js'
|
|
|
7
7
|
export * from './backup.plugin.js'
|
|
8
8
|
export * from './cache.plugin.js'
|
|
9
9
|
export * from './costs.plugin.js'
|
|
10
|
-
export * from './eventual-consistency.
|
|
10
|
+
export * from './eventual-consistency/index.js'
|
|
11
11
|
export * from './fulltext.plugin.js'
|
|
12
12
|
export * from './metrics.plugin.js'
|
|
13
13
|
export * from './queue-consumer.plugin.js'
|