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.
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Garbage collection for EventualConsistencyPlugin
3
+ * @module eventual-consistency/garbage-collection
4
+ */
5
+
6
+ import tryFn from "../../concerns/try-fn.js";
7
+ import { PromisePool } from "@supercharge/promise-pool";
8
+
9
+ /**
10
+ * Start garbage collection timer for a handler
11
+ *
12
+ * @param {Object} handler - Field handler
13
+ * @param {string} resourceName - Resource name
14
+ * @param {string} fieldName - Field name
15
+ * @param {Function} runGCCallback - Callback to run GC
16
+ * @param {Object} config - Plugin configuration
17
+ * @returns {NodeJS.Timeout} GC timer
18
+ */
19
+ export function startGarbageCollectionTimer(handler, resourceName, fieldName, runGCCallback, config) {
20
+ const gcIntervalMs = config.gcInterval * 1000; // Convert seconds to ms
21
+
22
+ handler.gcTimer = setInterval(async () => {
23
+ await runGCCallback(handler, resourceName, fieldName);
24
+ }, gcIntervalMs);
25
+
26
+ return handler.gcTimer;
27
+ }
28
+
29
+ /**
30
+ * Delete old applied transactions based on retention policy
31
+ * Uses distributed locking to prevent multiple containers from running GC simultaneously
32
+ *
33
+ * @param {Object} transactionResource - Transaction resource
34
+ * @param {Object} lockResource - Lock resource
35
+ * @param {Object} config - Plugin configuration
36
+ * @param {Function} emitFn - Function to emit events
37
+ * @returns {Promise<void>}
38
+ */
39
+ export async function runGarbageCollection(transactionResource, lockResource, config, emitFn) {
40
+ // Acquire distributed lock for GC operation
41
+ const gcLockId = `lock-gc-${config.resource}-${config.field}`;
42
+ const [lockAcquired] = await tryFn(() =>
43
+ lockResource.insert({
44
+ id: gcLockId,
45
+ lockedAt: Date.now(),
46
+ workerId: process.pid ? String(process.pid) : 'unknown'
47
+ })
48
+ );
49
+
50
+ // If another container is already running GC, skip
51
+ if (!lockAcquired) {
52
+ if (config.verbose) {
53
+ console.log(`[EventualConsistency] GC already running in another container`);
54
+ }
55
+ return;
56
+ }
57
+
58
+ try {
59
+ const now = Date.now();
60
+ const retentionMs = config.transactionRetention * 24 * 60 * 60 * 1000; // Days to ms
61
+ const cutoffDate = new Date(now - retentionMs);
62
+ const cutoffIso = cutoffDate.toISOString();
63
+
64
+ if (config.verbose) {
65
+ console.log(`[EventualConsistency] Running GC for transactions older than ${cutoffIso} (${config.transactionRetention} days)`);
66
+ }
67
+
68
+ // Query old applied transactions
69
+ const [ok, err, oldTransactions] = await tryFn(() =>
70
+ transactionResource.query({
71
+ applied: true,
72
+ timestamp: { '<': cutoffIso }
73
+ })
74
+ );
75
+
76
+ if (!ok) {
77
+ if (config.verbose) {
78
+ console.warn(`[EventualConsistency] GC failed to query transactions:`, err?.message);
79
+ }
80
+ return;
81
+ }
82
+
83
+ if (!oldTransactions || oldTransactions.length === 0) {
84
+ if (config.verbose) {
85
+ console.log(`[EventualConsistency] No old transactions to clean up`);
86
+ }
87
+ return;
88
+ }
89
+
90
+ if (config.verbose) {
91
+ console.log(`[EventualConsistency] Deleting ${oldTransactions.length} old transactions`);
92
+ }
93
+
94
+ // Delete old transactions using PromisePool
95
+ const { results, errors } = await PromisePool
96
+ .for(oldTransactions)
97
+ .withConcurrency(10)
98
+ .process(async (txn) => {
99
+ const [deleted] = await tryFn(() => transactionResource.delete(txn.id));
100
+ return deleted;
101
+ });
102
+
103
+ if (config.verbose) {
104
+ console.log(`[EventualConsistency] GC completed: ${results.length} deleted, ${errors.length} errors`);
105
+ }
106
+
107
+ if (emitFn) {
108
+ emitFn('eventual-consistency.gc-completed', {
109
+ resource: config.resource,
110
+ field: config.field,
111
+ deletedCount: results.length,
112
+ errorCount: errors.length
113
+ });
114
+ }
115
+ } catch (error) {
116
+ if (config.verbose) {
117
+ console.warn(`[EventualConsistency] GC error:`, error.message);
118
+ }
119
+ if (emitFn) {
120
+ emitFn('eventual-consistency.gc-error', error);
121
+ }
122
+ } finally {
123
+ // Always release GC lock
124
+ await tryFn(() => lockResource.delete(gcLockId));
125
+ }
126
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Helper methods added to resources for EventualConsistencyPlugin
3
+ * @module eventual-consistency/helpers
4
+ */
5
+
6
+ import { idGenerator } from "../../concerns/id.js";
7
+ import tryFn from "../../concerns/try-fn.js";
8
+ import { getCohortInfo, resolveFieldAndPlugin } from "./utils.js";
9
+
10
+ /**
11
+ * Add helper methods to resources
12
+ * This adds: set(), add(), sub(), consolidate(), getConsolidatedValue(), recalculate()
13
+ *
14
+ * @param {Object} resource - Resource to add methods to
15
+ * @param {Object} plugin - Plugin instance
16
+ * @param {Object} config - Plugin configuration
17
+ */
18
+ export function addHelperMethods(resource, plugin, config) {
19
+ // Add method to set value (replaces current value)
20
+ // Signature: set(id, field, value)
21
+ resource.set = async (id, field, value) => {
22
+ const { plugin: handler } = resolveFieldAndPlugin(resource, field, value);
23
+
24
+ // Create transaction inline
25
+ const now = new Date();
26
+ const cohortInfo = getCohortInfo(now, config.cohort.timezone, config.verbose);
27
+
28
+ const transaction = {
29
+ id: idGenerator(),
30
+ originalId: id,
31
+ field: handler.field,
32
+ value: value,
33
+ operation: 'set',
34
+ timestamp: now.toISOString(),
35
+ cohortDate: cohortInfo.date,
36
+ cohortHour: cohortInfo.hour,
37
+ cohortMonth: cohortInfo.month,
38
+ source: 'set',
39
+ applied: false
40
+ };
41
+
42
+ await handler.transactionResource.insert(transaction);
43
+
44
+ // In sync mode, immediately consolidate
45
+ if (config.mode === 'sync') {
46
+ return await plugin._syncModeConsolidate(handler, id, field);
47
+ }
48
+
49
+ return value;
50
+ };
51
+
52
+ // Add method to increment value
53
+ // Signature: add(id, field, amount)
54
+ resource.add = async (id, field, amount) => {
55
+ const { plugin: handler } = resolveFieldAndPlugin(resource, field, amount);
56
+
57
+ // Create transaction inline
58
+ const now = new Date();
59
+ const cohortInfo = getCohortInfo(now, config.cohort.timezone, config.verbose);
60
+
61
+ const transaction = {
62
+ id: idGenerator(),
63
+ originalId: id,
64
+ field: handler.field,
65
+ value: amount,
66
+ operation: 'add',
67
+ timestamp: now.toISOString(),
68
+ cohortDate: cohortInfo.date,
69
+ cohortHour: cohortInfo.hour,
70
+ cohortMonth: cohortInfo.month,
71
+ source: 'add',
72
+ applied: false
73
+ };
74
+
75
+ await handler.transactionResource.insert(transaction);
76
+
77
+ // In sync mode, immediately consolidate
78
+ if (config.mode === 'sync') {
79
+ return await plugin._syncModeConsolidate(handler, id, field);
80
+ }
81
+
82
+ // Async mode - return current value (optimistic)
83
+ const [ok, err, record] = await tryFn(() => handler.targetResource.get(id));
84
+ const currentValue = (ok && record) ? (record[field] || 0) : 0;
85
+ return currentValue + amount;
86
+ };
87
+
88
+ // Add method to decrement value
89
+ // Signature: sub(id, field, amount)
90
+ resource.sub = async (id, field, amount) => {
91
+ const { plugin: handler } = resolveFieldAndPlugin(resource, field, amount);
92
+
93
+ // Create transaction inline
94
+ const now = new Date();
95
+ const cohortInfo = getCohortInfo(now, config.cohort.timezone, config.verbose);
96
+
97
+ const transaction = {
98
+ id: idGenerator(),
99
+ originalId: id,
100
+ field: handler.field,
101
+ value: amount,
102
+ operation: 'sub',
103
+ timestamp: now.toISOString(),
104
+ cohortDate: cohortInfo.date,
105
+ cohortHour: cohortInfo.hour,
106
+ cohortMonth: cohortInfo.month,
107
+ source: 'sub',
108
+ applied: false
109
+ };
110
+
111
+ await handler.transactionResource.insert(transaction);
112
+
113
+ // In sync mode, immediately consolidate
114
+ if (config.mode === 'sync') {
115
+ return await plugin._syncModeConsolidate(handler, id, field);
116
+ }
117
+
118
+ // Async mode - return current value (optimistic)
119
+ const [ok, err, record] = await tryFn(() => handler.targetResource.get(id));
120
+ const currentValue = (ok && record) ? (record[field] || 0) : 0;
121
+ return currentValue - amount;
122
+ };
123
+
124
+ // Add method to manually trigger consolidation
125
+ // Signature: consolidate(id, field)
126
+ resource.consolidate = async (id, field) => {
127
+ if (!field) {
128
+ throw new Error(`Field parameter is required: consolidate(id, field)`);
129
+ }
130
+
131
+ const handler = resource._eventualConsistencyPlugins[field];
132
+
133
+ if (!handler) {
134
+ const availableFields = Object.keys(resource._eventualConsistencyPlugins).join(', ');
135
+ throw new Error(
136
+ `No eventual consistency plugin found for field "${field}". ` +
137
+ `Available fields: ${availableFields}`
138
+ );
139
+ }
140
+
141
+ return await plugin._consolidateWithHandler(handler, id);
142
+ };
143
+
144
+ // Add method to get consolidated value without applying
145
+ // Signature: getConsolidatedValue(id, field, options)
146
+ resource.getConsolidatedValue = async (id, field, options = {}) => {
147
+ const handler = resource._eventualConsistencyPlugins[field];
148
+
149
+ if (!handler) {
150
+ const availableFields = Object.keys(resource._eventualConsistencyPlugins).join(', ');
151
+ throw new Error(
152
+ `No eventual consistency plugin found for field "${field}". ` +
153
+ `Available fields: ${availableFields}`
154
+ );
155
+ }
156
+
157
+ return await plugin._getConsolidatedValueWithHandler(handler, id, options);
158
+ };
159
+
160
+ // Add method to recalculate from scratch
161
+ // Signature: recalculate(id, field)
162
+ resource.recalculate = async (id, field) => {
163
+ if (!field) {
164
+ throw new Error(`Field parameter is required: recalculate(id, field)`);
165
+ }
166
+
167
+ const handler = resource._eventualConsistencyPlugins[field];
168
+
169
+ if (!handler) {
170
+ const availableFields = Object.keys(resource._eventualConsistencyPlugins).join(', ');
171
+ throw new Error(
172
+ `No eventual consistency plugin found for field "${field}". ` +
173
+ `Available fields: ${availableFields}`
174
+ );
175
+ }
176
+
177
+ return await plugin._recalculateWithHandler(handler, id);
178
+ };
179
+ }