s3db.js 6.2.0 → 7.0.1

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.
Files changed (60) hide show
  1. package/PLUGINS.md +2724 -0
  2. package/README.md +372 -469
  3. package/UNLICENSE +24 -0
  4. package/dist/s3db.cjs.js +12105 -19396
  5. package/dist/s3db.cjs.min.js +1 -1
  6. package/dist/s3db.d.ts +373 -72
  7. package/dist/s3db.es.js +12090 -19393
  8. package/dist/s3db.es.min.js +1 -1
  9. package/dist/s3db.iife.js +12103 -19398
  10. package/dist/s3db.iife.min.js +1 -1
  11. package/package.json +44 -38
  12. package/src/behaviors/body-only.js +110 -0
  13. package/src/behaviors/body-overflow.js +153 -0
  14. package/src/behaviors/enforce-limits.js +195 -0
  15. package/src/behaviors/index.js +39 -0
  16. package/src/behaviors/truncate-data.js +204 -0
  17. package/src/behaviors/user-managed.js +147 -0
  18. package/src/client.class.js +515 -0
  19. package/src/concerns/base62.js +61 -0
  20. package/src/concerns/calculator.js +204 -0
  21. package/src/concerns/crypto.js +159 -0
  22. package/src/concerns/id.js +8 -0
  23. package/src/concerns/index.js +5 -0
  24. package/src/concerns/try-fn.js +151 -0
  25. package/src/connection-string.class.js +75 -0
  26. package/src/database.class.js +599 -0
  27. package/src/errors.js +261 -0
  28. package/src/index.js +17 -0
  29. package/src/plugins/audit.plugin.js +442 -0
  30. package/src/plugins/cache/cache.class.js +53 -0
  31. package/src/plugins/cache/index.js +6 -0
  32. package/src/plugins/cache/memory-cache.class.js +164 -0
  33. package/src/plugins/cache/s3-cache.class.js +189 -0
  34. package/src/plugins/cache.plugin.js +275 -0
  35. package/src/plugins/consumers/index.js +24 -0
  36. package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
  37. package/src/plugins/consumers/sqs-consumer.js +102 -0
  38. package/src/plugins/costs.plugin.js +81 -0
  39. package/src/plugins/fulltext.plugin.js +473 -0
  40. package/src/plugins/index.js +12 -0
  41. package/src/plugins/metrics.plugin.js +603 -0
  42. package/src/plugins/plugin.class.js +210 -0
  43. package/src/plugins/plugin.obj.js +13 -0
  44. package/src/plugins/queue-consumer.plugin.js +134 -0
  45. package/src/plugins/replicator.plugin.js +769 -0
  46. package/src/plugins/replicators/base-replicator.class.js +85 -0
  47. package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
  48. package/src/plugins/replicators/index.js +44 -0
  49. package/src/plugins/replicators/postgres-replicator.class.js +427 -0
  50. package/src/plugins/replicators/s3db-replicator.class.js +352 -0
  51. package/src/plugins/replicators/sqs-replicator.class.js +427 -0
  52. package/src/resource.class.js +2626 -0
  53. package/src/s3db.d.ts +1263 -0
  54. package/src/schema.class.js +706 -0
  55. package/src/stream/index.js +16 -0
  56. package/src/stream/resource-ids-page-reader.class.js +10 -0
  57. package/src/stream/resource-ids-reader.class.js +63 -0
  58. package/src/stream/resource-reader.class.js +81 -0
  59. package/src/stream/resource-writer.class.js +92 -0
  60. package/src/validator.class.js +97 -0
@@ -0,0 +1,442 @@
1
+ import Plugin from "./plugin.class.js";
2
+ import tryFn, { tryFnSync } from "../concerns/try-fn.js";
3
+
4
+ export class AuditPlugin extends Plugin {
5
+ constructor(options = {}) {
6
+ super(options);
7
+ this.auditResource = null;
8
+ this.config = {
9
+ includeData: options.includeData !== false,
10
+ includePartitions: options.includePartitions !== false,
11
+ maxDataSize: options.maxDataSize || 10000, // 10KB limit
12
+ ...options
13
+ };
14
+ }
15
+
16
+ async onSetup() {
17
+
18
+ // Create audit resource if it doesn't exist
19
+ const [ok, err, auditResource] = await tryFn(() => this.database.createResource({
20
+ name: 'audits',
21
+ attributes: {
22
+ id: 'string|required',
23
+ resourceName: 'string|required',
24
+ operation: 'string|required',
25
+ recordId: 'string|required',
26
+ userId: 'string|optional',
27
+ timestamp: 'string|required',
28
+ oldData: 'string|optional',
29
+ newData: 'string|optional',
30
+ partition: 'string|optional',
31
+ partitionValues: 'string|optional',
32
+ metadata: 'string|optional'
33
+ },
34
+ behavior: 'body-overflow'
35
+ // keyPrefix removido
36
+ }));
37
+ this.auditResource = ok ? auditResource : (this.database.resources.audits || null);
38
+ if (!ok && !this.auditResource) return;
39
+
40
+ this.installDatabaseProxy();
41
+ this.installEventListeners();
42
+ }
43
+
44
+ async onStart() {
45
+ // Plugin is ready
46
+ }
47
+
48
+ async onStop() {
49
+ // Cleanup if needed
50
+ }
51
+
52
+ installDatabaseProxy() {
53
+ if (this.database._auditProxyInstalled) {
54
+ return; // Already installed
55
+ }
56
+
57
+ const installEventListenersForResource = this.installEventListenersForResource.bind(this);
58
+
59
+ // Store original method
60
+ this.database._originalCreateResource = this.database.createResource;
61
+
62
+ // Create new method that doesn't call itself
63
+ this.database.createResource = async function (...args) {
64
+ const resource = await this._originalCreateResource(...args);
65
+ if (resource.name !== 'audits') {
66
+ installEventListenersForResource(resource);
67
+ }
68
+ return resource;
69
+ };
70
+
71
+ // Mark as installed
72
+ this.database._auditProxyInstalled = true;
73
+ }
74
+
75
+ installEventListeners() {
76
+ for (const resource of Object.values(this.database.resources)) {
77
+ if (resource.name === 'audits') {
78
+ continue; // Don't audit the audit resource
79
+ }
80
+
81
+ this.installEventListenersForResource(resource);
82
+ }
83
+ }
84
+
85
+ installEventListenersForResource(resource) {
86
+ // Store original data for update operations
87
+ const originalDataMap = new Map();
88
+
89
+ // Insert event
90
+ resource.on('insert', async (data) => {
91
+ const recordId = data.id || 'auto-generated';
92
+
93
+ const partitionValues = this.config.includePartitions ? this.getPartitionValues(data, resource) : null;
94
+
95
+ const auditRecord = {
96
+ id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
97
+ resourceName: resource.name,
98
+ operation: 'insert',
99
+ recordId,
100
+ userId: this.getCurrentUserId?.() || 'system',
101
+ timestamp: new Date().toISOString(),
102
+ oldData: null,
103
+ newData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(data)),
104
+ partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
105
+ partitionValues: this.config.includePartitions ? (partitionValues ? (Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null) : null) : null,
106
+ metadata: JSON.stringify({
107
+ source: 'audit-plugin',
108
+ version: '2.0'
109
+ })
110
+ };
111
+
112
+ // Log audit asynchronously to avoid blocking
113
+ this.logAudit(auditRecord).catch(console.error);
114
+ });
115
+
116
+ // Update event
117
+ resource.on('update', async (data) => {
118
+ const recordId = data.id;
119
+ let oldData = data.$before;
120
+
121
+ if (this.config.includeData && !oldData) {
122
+ const [ok, err, fetched] = await tryFn(() => resource.get(recordId));
123
+ if (ok) oldData = fetched;
124
+ }
125
+
126
+ const partitionValues = this.config.includePartitions ? this.getPartitionValues(data, resource) : null;
127
+
128
+ const auditRecord = {
129
+ id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
130
+ resourceName: resource.name,
131
+ operation: 'update',
132
+ recordId,
133
+ userId: this.getCurrentUserId?.() || 'system',
134
+ timestamp: new Date().toISOString(),
135
+ oldData: oldData && this.config.includeData === false ? null : (oldData ? JSON.stringify(this.truncateData(oldData)) : null),
136
+ newData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(data)),
137
+ partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
138
+ partitionValues: this.config.includePartitions ? (partitionValues ? (Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null) : null) : null,
139
+ metadata: JSON.stringify({
140
+ source: 'audit-plugin',
141
+ version: '2.0'
142
+ })
143
+ };
144
+
145
+ // Log audit asynchronously
146
+ this.logAudit(auditRecord).catch(console.error);
147
+ });
148
+
149
+ // Delete event
150
+ resource.on('delete', async (data) => {
151
+ const recordId = data.id;
152
+ let oldData = data;
153
+
154
+ if (this.config.includeData && !oldData) {
155
+ const [ok, err, fetched] = await tryFn(() => resource.get(recordId));
156
+ if (ok) oldData = fetched;
157
+ }
158
+
159
+ const partitionValues = oldData && this.config.includePartitions ? this.getPartitionValues(oldData, resource) : null;
160
+
161
+ const auditRecord = {
162
+ id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
163
+ resourceName: resource.name,
164
+ operation: 'delete',
165
+ recordId,
166
+ userId: this.getCurrentUserId?.() || 'system',
167
+ timestamp: new Date().toISOString(),
168
+ oldData: oldData && this.config.includeData === false ? null : (oldData ? JSON.stringify(this.truncateData(oldData)) : null),
169
+ newData: null,
170
+ partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
171
+ partitionValues: this.config.includePartitions ? (partitionValues ? (Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null) : null) : null,
172
+ metadata: JSON.stringify({
173
+ source: 'audit-plugin',
174
+ version: '2.0'
175
+ })
176
+ };
177
+
178
+ // Log audit asynchronously
179
+ this.logAudit(auditRecord).catch(console.error);
180
+ });
181
+
182
+ // Remover monkey patch de deleteMany
183
+ // Adicionar middleware para deleteMany
184
+ resource.useMiddleware('deleteMany', async (ctx, next) => {
185
+ const ids = ctx.args[0];
186
+ // Captura os dados antes da deleção
187
+ const oldDataMap = {};
188
+ if (this.config.includeData) {
189
+ for (const id of ids) {
190
+ const [ok, err, data] = await tryFn(() => resource.get(id));
191
+ oldDataMap[id] = ok ? data : null;
192
+ }
193
+ }
194
+ const result = await next();
195
+ // Auditar depois
196
+ if (result && result.length > 0 && this.config.includeData) {
197
+ for (const id of ids) {
198
+ const oldData = oldDataMap[id];
199
+ const partitionValues = oldData ? (this.config.includePartitions ? this.getPartitionValues(oldData, resource) : null) : null;
200
+ const auditRecord = {
201
+ id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
202
+ resourceName: resource.name,
203
+ operation: 'delete',
204
+ recordId: id,
205
+ userId: this.getCurrentUserId?.() || 'system',
206
+ timestamp: new Date().toISOString(),
207
+ oldData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(oldData)),
208
+ newData: null,
209
+ partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
210
+ partitionValues: this.config.includePartitions ? (partitionValues ? (Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null) : null) : null,
211
+ metadata: JSON.stringify({
212
+ source: 'audit-plugin',
213
+ version: '2.0',
214
+ batchOperation: true
215
+ })
216
+ };
217
+ this.logAudit(auditRecord).catch(console.error);
218
+ }
219
+ }
220
+ return result;
221
+ });
222
+ }
223
+
224
+ getPartitionValues(data, resource) {
225
+ if (!data) return null;
226
+ const partitions = resource.config?.partitions || {};
227
+ const partitionValues = {};
228
+
229
+ for (const [partitionName, partitionDef] of Object.entries(partitions)) {
230
+ if (partitionDef.fields) {
231
+ const partitionData = {};
232
+ for (const [fieldName, fieldRule] of Object.entries(partitionDef.fields)) {
233
+ // Handle nested fields using dot notation
234
+ const fieldValue = this.getNestedFieldValue(data, fieldName);
235
+ if (fieldValue !== undefined && fieldValue !== null) {
236
+ partitionData[fieldName] = fieldValue;
237
+ }
238
+ }
239
+ if (Object.keys(partitionData).length > 0) {
240
+ partitionValues[partitionName] = partitionData;
241
+ }
242
+ }
243
+ }
244
+
245
+ return partitionValues;
246
+ }
247
+
248
+ getNestedFieldValue(data, fieldPath) {
249
+ // Handle simple field names (no dots)
250
+ if (!fieldPath.includes('.')) {
251
+ return data[fieldPath];
252
+ }
253
+
254
+ // Handle nested field names using dot notation
255
+ const keys = fieldPath.split('.');
256
+ let currentLevel = data;
257
+
258
+ for (const key of keys) {
259
+ if (!currentLevel || typeof currentLevel !== 'object' || !(key in currentLevel)) {
260
+ return undefined;
261
+ }
262
+ currentLevel = currentLevel[key];
263
+ }
264
+
265
+ return currentLevel;
266
+ }
267
+
268
+ getPrimaryPartition(partitionValues) {
269
+ if (!partitionValues) return null;
270
+ const partitionNames = Object.keys(partitionValues);
271
+ return partitionNames.length > 0 ? partitionNames[0] : null;
272
+ }
273
+
274
+ async logAudit(auditRecord) {
275
+ if (!auditRecord.id) {
276
+ auditRecord.id = `audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
277
+ }
278
+ const result = await this.auditResource.insert(auditRecord);
279
+ return result;
280
+ }
281
+
282
+ truncateData(data) {
283
+ if (!data) return data;
284
+
285
+ // Filter out internal S3DB fields (those starting with _)
286
+ const filteredData = {};
287
+ for (const [key, value] of Object.entries(data)) {
288
+ if (!key.startsWith('_') && key !== '$overflow') {
289
+ filteredData[key] = value;
290
+ }
291
+ }
292
+
293
+ const dataStr = JSON.stringify(filteredData);
294
+ if (dataStr.length <= this.config.maxDataSize) {
295
+ return filteredData;
296
+ }
297
+
298
+ // Need to actually truncate the data to fit within maxDataSize
299
+ let truncatedData = { ...filteredData };
300
+ let currentSize = JSON.stringify(truncatedData).length;
301
+
302
+ // Reserve space for truncation metadata
303
+ const metadataOverhead = JSON.stringify({
304
+ _truncated: true,
305
+ _originalSize: dataStr.length,
306
+ _truncatedAt: new Date().toISOString()
307
+ }).length;
308
+
309
+ const targetSize = this.config.maxDataSize - metadataOverhead;
310
+
311
+ // Truncate string fields until we fit within the limit
312
+ for (const [key, value] of Object.entries(truncatedData)) {
313
+ if (typeof value === 'string' && currentSize > targetSize) {
314
+ const excess = currentSize - targetSize;
315
+ const newLength = Math.max(0, value.length - excess - 3); // -3 for "..."
316
+ if (newLength < value.length) {
317
+ truncatedData[key] = value.substring(0, newLength) + '...';
318
+ currentSize = JSON.stringify(truncatedData).length;
319
+ }
320
+ }
321
+ }
322
+
323
+ return {
324
+ ...truncatedData,
325
+ _truncated: true,
326
+ _originalSize: dataStr.length,
327
+ _truncatedAt: new Date().toISOString()
328
+ };
329
+ }
330
+
331
+ // Utility methods for querying audit logs
332
+ async getAuditLogs(options = {}) {
333
+ if (!this.auditResource) return [];
334
+ const [ok, err, result] = await tryFn(async () => {
335
+ const {
336
+ resourceName,
337
+ operation,
338
+ recordId,
339
+ userId,
340
+ partition,
341
+ startDate,
342
+ endDate,
343
+ limit = 100,
344
+ offset = 0
345
+ } = options;
346
+
347
+ const allAudits = await this.auditResource.getAll();
348
+ let filtered = allAudits.filter(audit => {
349
+ if (resourceName && audit.resourceName !== resourceName) return false;
350
+ if (operation && audit.operation !== operation) return false;
351
+ if (recordId && audit.recordId !== recordId) return false;
352
+ if (userId && audit.userId !== userId) return false;
353
+ if (partition && audit.partition !== partition) return false;
354
+ if (startDate && new Date(audit.timestamp) < new Date(startDate)) return false;
355
+ if (endDate && new Date(audit.timestamp) > new Date(endDate)) return false;
356
+ return true;
357
+ });
358
+ filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
359
+ const deserialized = filtered.slice(offset, offset + limit).map(audit => {
360
+ const [okOld, , oldData] = typeof audit.oldData === 'string' ? tryFnSync(() => JSON.parse(audit.oldData)) : [true, null, audit.oldData];
361
+ const [okNew, , newData] = typeof audit.newData === 'string' ? tryFnSync(() => JSON.parse(audit.newData)) : [true, null, audit.newData];
362
+ const [okPart, , partitionValues] = audit.partitionValues && typeof audit.partitionValues === 'string' ? tryFnSync(() => JSON.parse(audit.partitionValues)) : [true, null, audit.partitionValues];
363
+ const [okMeta, , metadata] = audit.metadata && typeof audit.metadata === 'string' ? tryFnSync(() => JSON.parse(audit.metadata)) : [true, null, audit.metadata];
364
+ return {
365
+ ...audit,
366
+ oldData: audit.oldData === null || audit.oldData === undefined || audit.oldData === 'null' ? null : (okOld ? oldData : null),
367
+ newData: audit.newData === null || audit.newData === undefined || audit.newData === 'null' ? null : (okNew ? newData : null),
368
+ partitionValues: okPart ? partitionValues : audit.partitionValues,
369
+ metadata: okMeta ? metadata : audit.metadata
370
+ };
371
+ });
372
+ return deserialized;
373
+ });
374
+ return ok ? result : [];
375
+ }
376
+
377
+ async getRecordHistory(resourceName, recordId) {
378
+ return this.getAuditLogs({
379
+ resourceName,
380
+ recordId,
381
+ limit: 1000
382
+ });
383
+ }
384
+
385
+ async getPartitionHistory(resourceName, partitionName, partitionValues) {
386
+ return this.getAuditLogs({
387
+ resourceName,
388
+ partition: partitionName,
389
+ limit: 1000
390
+ });
391
+ }
392
+
393
+ async getAuditStats(options = {}) {
394
+ const {
395
+ resourceName,
396
+ startDate,
397
+ endDate
398
+ } = options;
399
+
400
+ const allAudits = await this.getAuditLogs({
401
+ resourceName,
402
+ startDate,
403
+ endDate,
404
+ limit: 10000
405
+ });
406
+
407
+ const stats = {
408
+ total: allAudits.length,
409
+ byOperation: {},
410
+ byResource: {},
411
+ byPartition: {},
412
+ byUser: {},
413
+ timeline: {}
414
+ };
415
+
416
+ for (const audit of allAudits) {
417
+ // Count by operation
418
+ stats.byOperation[audit.operation] = (stats.byOperation[audit.operation] || 0) + 1;
419
+
420
+ // Count by resource
421
+ stats.byResource[audit.resourceName] = (stats.byResource[audit.resourceName] || 0) + 1;
422
+
423
+ // Count by partition
424
+ if (audit.partition) {
425
+ stats.byPartition[audit.partition] = (stats.byPartition[audit.partition] || 0) + 1;
426
+ }
427
+
428
+ // Count by user
429
+ stats.byUser[audit.userId] = (stats.byUser[audit.userId] || 0) + 1;
430
+
431
+ // Count by day
432
+ if (audit.timestamp) {
433
+ const day = audit.timestamp.split('T')[0];
434
+ stats.timeline[day] = (stats.timeline[day] || 0) + 1;
435
+ }
436
+ }
437
+
438
+ return stats;
439
+ }
440
+ }
441
+
442
+ export default AuditPlugin;
@@ -0,0 +1,53 @@
1
+ import EventEmitter from "events";
2
+
3
+ export class Cache extends EventEmitter {
4
+ constructor(config = {}) {
5
+ super();
6
+ this.config = config;
7
+ }
8
+ // to implement:
9
+ async _set (key, data) {}
10
+ async _get (key) {}
11
+ async _del (key) {}
12
+ async _clear (key) {}
13
+
14
+ validateKey(key) {
15
+ if (key === null || key === undefined || typeof key !== 'string' || !key) {
16
+ throw new Error('Invalid key');
17
+ }
18
+ }
19
+
20
+ // generic class methods
21
+ async set(key, data) {
22
+ this.validateKey(key);
23
+ await this._set(key, data);
24
+ this.emit("set", data);
25
+ return data
26
+ }
27
+
28
+ async get(key) {
29
+ this.validateKey(key);
30
+ const data = await this._get(key);
31
+ this.emit("get", data);
32
+ return data;
33
+ }
34
+
35
+ async del(key) {
36
+ this.validateKey(key);
37
+ const data = await this._del(key);
38
+ this.emit("delete", data);
39
+ return data;
40
+ }
41
+
42
+ async delete(key) {
43
+ return this.del(key);
44
+ }
45
+
46
+ async clear(prefix) {
47
+ const data = await this._clear(prefix);
48
+ this.emit("clear", data);
49
+ return data;
50
+ }
51
+ }
52
+
53
+ export default Cache
@@ -0,0 +1,6 @@
1
+ export * from "./cache.class.js"
2
+ export * from "./memory-cache.class.js"
3
+ export * from "./s3-cache.class.js"
4
+
5
+ export { default as S3Cache } from './s3-cache.class.js';
6
+ export { default as MemoryCache } from './memory-cache.class.js';
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Memory Cache Configuration Documentation
3
+ *
4
+ * This cache implementation stores data in memory using a Map-like structure.
5
+ * It provides fast access to frequently used data but is limited by available RAM
6
+ * and data is lost when the process restarts.
7
+ *
8
+ * @typedef {Object} MemoryCacheConfig
9
+ * @property {number} [maxSize=1000] - Maximum number of items to store in cache
10
+ * @property {number} [ttl=300000] - Time to live in milliseconds (5 minutes default)
11
+ * @property {boolean} [enableStats=false] - Whether to track cache statistics (hits, misses, etc.)
12
+ * @property {string} [evictionPolicy='lru'] - Cache eviction policy: 'lru' (Least Recently Used) or 'fifo' (First In First Out)
13
+ * @property {boolean} [logEvictions=false] - Whether to log when items are evicted from cache
14
+ * @property {number} [cleanupInterval=60000] - Interval in milliseconds to run cleanup of expired items (1 minute default)
15
+ * @property {boolean} [caseSensitive=true] - Whether cache keys are case sensitive
16
+ * @property {Function} [serializer] - Custom function to serialize values before storage
17
+ * - Parameters: (value: any) => string
18
+ * - Default: JSON.stringify
19
+ * @property {Function} [deserializer] - Custom function to deserialize values after retrieval
20
+ * - Parameters: (string: string) => any
21
+ * - Default: JSON.parse
22
+ * @property {boolean} [enableCompression=false] - Whether to compress values using gzip (requires zlib)
23
+ * @property {number} [compressionThreshold=1024] - Minimum size in bytes to trigger compression
24
+ * @property {Object} [tags] - Default tags to apply to all cached items
25
+ * - Key: tag name (e.g., 'environment', 'version')
26
+ * - Value: tag value (e.g., 'production', '1.0.0')
27
+ * @property {boolean} [persistent=false] - Whether to persist cache to disk (experimental)
28
+ * @property {string} [persistencePath='./cache'] - Directory path for persistent cache storage
29
+ * @property {number} [persistenceInterval=300000] - Interval in milliseconds to save cache to disk (5 minutes default)
30
+ *
31
+ * @example
32
+ * // Basic configuration with LRU eviction
33
+ * {
34
+ * maxSize: 5000,
35
+ * ttl: 600000, // 10 minutes
36
+ * evictionPolicy: 'lru',
37
+ * enableStats: true,
38
+ * logEvictions: true
39
+ * }
40
+ *
41
+ * @example
42
+ * // Configuration with compression and custom serialization
43
+ * {
44
+ * maxSize: 10000,
45
+ * ttl: 1800000, // 30 minutes
46
+ * enableCompression: true,
47
+ * compressionThreshold: 512,
48
+ * serializer: (value) => Buffer.from(JSON.stringify(value)).toString('base64'),
49
+ * deserializer: (str) => JSON.parse(Buffer.from(str, 'base64').toString()),
50
+ * tags: {
51
+ * 'environment': 'production',
52
+ * 'cache_type': 'memory'
53
+ * }
54
+ * }
55
+ *
56
+ * @example
57
+ * // FIFO configuration with persistent storage
58
+ * {
59
+ * maxSize: 2000,
60
+ * ttl: 900000, // 15 minutes
61
+ * evictionPolicy: 'fifo',
62
+ * persistent: true,
63
+ * persistencePath: './data/cache',
64
+ * persistenceInterval: 600000 // 10 minutes
65
+ * }
66
+ *
67
+ * @example
68
+ * // Minimal configuration using defaults
69
+ * {
70
+ * maxSize: 1000,
71
+ * ttl: 300000 // 5 minutes
72
+ * }
73
+ *
74
+ * @notes
75
+ * - Memory usage is limited by available RAM and maxSize setting
76
+ * - TTL is checked on access, not automatically in background
77
+ * - LRU eviction removes least recently accessed items when cache is full
78
+ * - FIFO eviction removes oldest items when cache is full
79
+ * - Statistics include hit rate, miss rate, and eviction count
80
+ * - Compression reduces memory usage but increases CPU overhead
81
+ * - Custom serializers allow for specialized data formats
82
+ * - Persistent storage survives process restarts but may be slower
83
+ * - Cleanup interval helps prevent memory leaks from expired items
84
+ * - Tags are useful for cache invalidation and monitoring
85
+ * - Case sensitivity affects key matching and storage efficiency
86
+ */
87
+ import { Cache } from "./cache.class.js"
88
+
89
+ export class MemoryCache extends Cache {
90
+ constructor(config = {}) {
91
+ super(config);
92
+ this.cache = {};
93
+ this.meta = {};
94
+ this.maxSize = config.maxSize || 0;
95
+ this.ttl = config.ttl || 0;
96
+ }
97
+
98
+ async _set(key, data) {
99
+ // Limpar se exceder maxSize
100
+ if (this.maxSize > 0 && Object.keys(this.cache).length >= this.maxSize) {
101
+ // Remove o item mais antigo
102
+ const oldestKey = Object.entries(this.meta)
103
+ .sort((a, b) => a[1].ts - b[1].ts)[0]?.[0];
104
+ if (oldestKey) {
105
+ delete this.cache[oldestKey];
106
+ delete this.meta[oldestKey];
107
+ }
108
+ }
109
+ this.cache[key] = data;
110
+ this.meta[key] = { ts: Date.now() };
111
+ return data;
112
+ }
113
+
114
+ async _get(key) {
115
+ if (!Object.prototype.hasOwnProperty.call(this.cache, key)) return null;
116
+ if (this.ttl > 0) {
117
+ const now = Date.now();
118
+ const meta = this.meta[key];
119
+ if (meta && now - meta.ts > this.ttl * 1000) {
120
+ // Expirado
121
+ delete this.cache[key];
122
+ delete this.meta[key];
123
+ return null;
124
+ }
125
+ }
126
+ return this.cache[key];
127
+ }
128
+
129
+ async _del(key) {
130
+ delete this.cache[key];
131
+ delete this.meta[key];
132
+ return true;
133
+ }
134
+
135
+ async _clear(prefix) {
136
+ if (!prefix) {
137
+ this.cache = {};
138
+ this.meta = {};
139
+ return true;
140
+ }
141
+ // Remove only keys that start with the prefix
142
+ const removed = [];
143
+ for (const key of Object.keys(this.cache)) {
144
+ if (key.startsWith(prefix)) {
145
+ removed.push(key);
146
+ delete this.cache[key];
147
+ delete this.meta[key];
148
+ }
149
+ }
150
+ if (removed.length > 0) {
151
+ }
152
+ return true;
153
+ }
154
+
155
+ async size() {
156
+ return Object.keys(this.cache).length;
157
+ }
158
+
159
+ async keys() {
160
+ return Object.keys(this.cache);
161
+ }
162
+ }
163
+
164
+ export default MemoryCache