s3db.js 9.3.0 → 10.0.0

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 CHANGED
@@ -329,11 +329,65 @@ const s3db = new S3db({
329
329
  });
330
330
  ```
331
331
 
332
- #### 3. S3-Compatible Services (MinIO, etc.)
332
+ #### 3. MinIO (Self-hosted S3)
333
333
  ```javascript
334
+ // MinIO running locally (note: http:// protocol and port)
334
335
  const s3db = new S3db({
335
- connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
336
- endpoint: "http://localhost:9000"
336
+ connectionString: "http://minioadmin:minioadmin@localhost:9000/mybucket/databases/myapp"
337
+ });
338
+
339
+ // MinIO on custom server
340
+ const s3db = new S3db({
341
+ connectionString: "http://ACCESS_KEY:SECRET_KEY@minio.example.com:9000/BUCKET_NAME/databases/myapp"
342
+ });
343
+ ```
344
+
345
+ #### 4. Digital Ocean Spaces (SaaS)
346
+ ```javascript
347
+ // Digital Ocean Spaces (NYC3 datacenter) - uses https:// as it's a public service
348
+ const s3db = new S3db({
349
+ connectionString: "https://SPACES_KEY:SPACES_SECRET@nyc3.digitaloceanspaces.com/SPACE_NAME/databases/myapp"
350
+ });
351
+
352
+ // Other regions available: sfo3, ams3, sgp1, fra1, syd1
353
+ const s3db = new S3db({
354
+ connectionString: "https://SPACES_KEY:SPACES_SECRET@sgp1.digitaloceanspaces.com/SPACE_NAME/databases/myapp"
355
+ });
356
+ ```
357
+
358
+ #### 5. LocalStack (Local AWS testing)
359
+ ```javascript
360
+ // LocalStack for local development/testing (http:// with port 4566)
361
+ const s3db = new S3db({
362
+ connectionString: "http://test:test@localhost:4566/mybucket/databases/myapp"
363
+ });
364
+
365
+ // LocalStack in Docker container
366
+ const s3db = new S3db({
367
+ connectionString: "http://test:test@localstack:4566/mybucket/databases/myapp"
368
+ });
369
+ ```
370
+
371
+ #### 6. Other S3-Compatible Services
372
+ ```javascript
373
+ // Backblaze B2 (SaaS - uses https://)
374
+ const s3db = new S3db({
375
+ connectionString: "https://KEY_ID:APPLICATION_KEY@s3.us-west-002.backblazeb2.com/BUCKET_NAME/databases/myapp"
376
+ });
377
+
378
+ // Wasabi (SaaS - uses https://)
379
+ const s3db = new S3db({
380
+ connectionString: "https://ACCESS_KEY:SECRET_KEY@s3.wasabisys.com/BUCKET_NAME/databases/myapp"
381
+ });
382
+
383
+ // Cloudflare R2 (SaaS - uses https://)
384
+ const s3db = new S3db({
385
+ connectionString: "https://ACCESS_KEY:SECRET_KEY@ACCOUNT_ID.r2.cloudflarestorage.com/BUCKET_NAME/databases/myapp"
386
+ });
387
+
388
+ // Self-hosted Ceph with S3 gateway (http:// with custom port)
389
+ const s3db = new S3db({
390
+ connectionString: "http://ACCESS_KEY:SECRET_KEY@ceph.internal:7480/BUCKET_NAME/databases/myapp"
337
391
  });
338
392
  ```
339
393
 
@@ -732,16 +786,20 @@ await users.insert({ name: "John", email: "john@example.com" });
732
786
 
733
787
  #### Available Plugins
734
788
 
735
- - **💾 Cache Plugin** - Intelligent caching (memory/S3) for performance
736
- - **💰 Costs Plugin** - Real-time AWS S3 cost tracking
737
- - **🔍 FullText Plugin** - Advanced search with automatic indexing
738
- - **📊 Metrics Plugin** - Performance monitoring and analytics
739
- - **🔄 Replicator Plugin** - Multi-target replication (S3DB, SQS, BigQuery, PostgreSQL)
740
- - **📝 Audit Plugin** - Comprehensive audit logging for compliance
741
- - **📬 Queue Consumer Plugin** - Message consumption from SQS/RabbitMQ
742
-
743
- **📖 For detailed documentation, configuration options, and advanced examples, see:**
744
- **[📋 Complete Plugin Documentation](https://github.com/forattini-dev/s3db.js/blob/main/PLUGINS.md)**
789
+ - **💾 [Cache Plugin](./docs/plugins/cache.md)** - Intelligent caching (memory/S3) for performance
790
+ - **💰 [Costs Plugin](./docs/plugins/costs.md)** - Real-time AWS S3 cost tracking
791
+ - **🔍 [FullText Plugin](./docs/plugins/fulltext.md)** - Advanced search with automatic indexing
792
+ - **📊 [Metrics Plugin](./docs/plugins/metrics.md)** - Performance monitoring and analytics
793
+ - **🔄 [Replicator Plugin](./docs/plugins/replicator.md)** - Multi-target replication (S3DB, SQS, BigQuery, PostgreSQL)
794
+ - **📝 [Audit Plugin](./docs/plugins/audit.md)** - Comprehensive audit logging for compliance
795
+ - **📬 [Queue Consumer Plugin](./docs/plugins/queue-consumer.md)** - Message consumption from SQS/RabbitMQ
796
+ - **📈 [Eventual Consistency Plugin](./docs/plugins/eventual-consistency.md)** - Event sourcing for numeric fields
797
+ - **📅 [Scheduler Plugin](./docs/plugins/scheduler.md)** - Task scheduling and automation
798
+ - **🔄 [State Machine Plugin](./docs/plugins/state-machine.md)** - State management and transitions
799
+ - **💾 [Backup Plugin](./docs/plugins/backup.md)** - Backup and restore functionality
800
+
801
+ **📖 For complete plugin documentation and overview:**
802
+ **[📋 Plugin Documentation Index](./docs/plugins/README.md)**
745
803
 
746
804
  ### 🎛️ Resource Behaviors
747
805
 
package/dist/s3db.cjs.js CHANGED
@@ -423,8 +423,14 @@ function mapAwsError(err, context = {}) {
423
423
  suggestion = "Check if the object metadata is present and valid.";
424
424
  return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput, suggestion });
425
425
  }
426
- suggestion = "Check the error details and AWS documentation.";
427
- return new UnknownError("Unknown error", { ...context, original: err, metadata, commandName, commandInput, suggestion });
426
+ const errorDetails = [
427
+ `Unknown error: ${err.message || err.toString()}`,
428
+ err.code && `Code: ${err.code}`,
429
+ err.statusCode && `Status: ${err.statusCode}`,
430
+ err.stack && `Stack: ${err.stack.split("\n")[0]}`
431
+ ].filter(Boolean).join(" | ");
432
+ suggestion = `Check the error details and AWS documentation. Original error: ${err.message || err.toString()}`;
433
+ return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput, suggestion });
428
434
  }
429
435
  class ConnectionStringError extends S3dbError {
430
436
  constructor(message, details = {}) {
@@ -1903,7 +1909,7 @@ class BackupPlugin extends Plugin {
1903
1909
  include: options.include || null,
1904
1910
  exclude: options.exclude || [],
1905
1911
  backupMetadataResource: options.backupMetadataResource || "backup_metadata",
1906
- tempDir: options.tempDir || "./tmp/backups",
1912
+ tempDir: options.tempDir || "/tmp/s3db/backups",
1907
1913
  verbose: options.verbose || false,
1908
1914
  // Hooks
1909
1915
  onBackupStart: options.onBackupStart || null,
@@ -4050,6 +4056,457 @@ const CostsPlugin = {
4050
4056
  }
4051
4057
  };
4052
4058
 
4059
+ class EventualConsistencyPlugin extends Plugin {
4060
+ constructor(options = {}) {
4061
+ super(options);
4062
+ if (!options.resource) {
4063
+ throw new Error("EventualConsistencyPlugin requires 'resource' option");
4064
+ }
4065
+ if (!options.field) {
4066
+ throw new Error("EventualConsistencyPlugin requires 'field' option");
4067
+ }
4068
+ this.config = {
4069
+ resource: options.resource,
4070
+ field: options.field,
4071
+ cohort: {
4072
+ interval: options.cohort?.interval || "24h",
4073
+ timezone: options.cohort?.timezone || "UTC",
4074
+ ...options.cohort
4075
+ },
4076
+ reducer: options.reducer || ((transactions) => {
4077
+ let baseValue = 0;
4078
+ for (const t of transactions) {
4079
+ if (t.operation === "set") {
4080
+ baseValue = t.value;
4081
+ } else if (t.operation === "add") {
4082
+ baseValue += t.value;
4083
+ } else if (t.operation === "sub") {
4084
+ baseValue -= t.value;
4085
+ }
4086
+ }
4087
+ return baseValue;
4088
+ }),
4089
+ consolidationInterval: options.consolidationInterval || 36e5,
4090
+ // 1 hour default
4091
+ autoConsolidate: options.autoConsolidate !== false,
4092
+ batchTransactions: options.batchTransactions || false,
4093
+ batchSize: options.batchSize || 100,
4094
+ mode: options.mode || "async",
4095
+ // 'async' or 'sync'
4096
+ ...options
4097
+ };
4098
+ this.transactionResource = null;
4099
+ this.targetResource = null;
4100
+ this.consolidationTimer = null;
4101
+ this.pendingTransactions = /* @__PURE__ */ new Map();
4102
+ }
4103
+ async onSetup() {
4104
+ this.targetResource = this.database.resources[this.config.resource];
4105
+ if (!this.targetResource) {
4106
+ this.deferredSetup = true;
4107
+ this.watchForResource();
4108
+ return;
4109
+ }
4110
+ await this.completeSetup();
4111
+ }
4112
+ watchForResource() {
4113
+ const hookCallback = async ({ resource, config }) => {
4114
+ if (config.name === this.config.resource && this.deferredSetup) {
4115
+ this.targetResource = resource;
4116
+ this.deferredSetup = false;
4117
+ await this.completeSetup();
4118
+ }
4119
+ };
4120
+ this.database.addHook("afterCreateResource", hookCallback);
4121
+ }
4122
+ async completeSetup() {
4123
+ if (!this.targetResource) return;
4124
+ const transactionResourceName = `${this.config.resource}_transactions_${this.config.field}`;
4125
+ const partitionConfig = this.createPartitionConfig();
4126
+ const [ok, err, transactionResource] = await tryFn(
4127
+ () => this.database.createResource({
4128
+ name: transactionResourceName,
4129
+ attributes: {
4130
+ id: "string|required",
4131
+ originalId: "string|required",
4132
+ field: "string|required",
4133
+ value: "number|required",
4134
+ operation: "string|required",
4135
+ // 'set', 'add', or 'sub'
4136
+ timestamp: "string|required",
4137
+ cohortDate: "string|required",
4138
+ // For partitioning
4139
+ cohortMonth: "string|optional",
4140
+ // For monthly partitioning
4141
+ source: "string|optional",
4142
+ applied: "boolean|optional"
4143
+ // Track if transaction was applied
4144
+ },
4145
+ behavior: "body-overflow",
4146
+ timestamps: true,
4147
+ partitions: partitionConfig,
4148
+ asyncPartitions: true
4149
+ // Use async partitions for better performance
4150
+ })
4151
+ );
4152
+ if (!ok && !this.database.resources[transactionResourceName]) {
4153
+ throw new Error(`Failed to create transaction resource: ${err?.message}`);
4154
+ }
4155
+ this.transactionResource = ok ? transactionResource : this.database.resources[transactionResourceName];
4156
+ this.addHelperMethods();
4157
+ if (this.config.autoConsolidate) {
4158
+ this.startConsolidationTimer();
4159
+ }
4160
+ }
4161
+ async onStart() {
4162
+ if (this.deferredSetup) {
4163
+ return;
4164
+ }
4165
+ this.emit("eventual-consistency.started", {
4166
+ resource: this.config.resource,
4167
+ field: this.config.field,
4168
+ cohort: this.config.cohort
4169
+ });
4170
+ }
4171
+ async onStop() {
4172
+ if (this.consolidationTimer) {
4173
+ clearInterval(this.consolidationTimer);
4174
+ this.consolidationTimer = null;
4175
+ }
4176
+ await this.flushPendingTransactions();
4177
+ this.emit("eventual-consistency.stopped", {
4178
+ resource: this.config.resource,
4179
+ field: this.config.field
4180
+ });
4181
+ }
4182
+ createPartitionConfig() {
4183
+ const partitions = {
4184
+ byDay: {
4185
+ fields: {
4186
+ cohortDate: "string"
4187
+ }
4188
+ },
4189
+ byMonth: {
4190
+ fields: {
4191
+ cohortMonth: "string"
4192
+ }
4193
+ }
4194
+ };
4195
+ return partitions;
4196
+ }
4197
+ addHelperMethods() {
4198
+ const resource = this.targetResource;
4199
+ const defaultField = this.config.field;
4200
+ const plugin = this;
4201
+ if (!resource._eventualConsistencyPlugins) {
4202
+ resource._eventualConsistencyPlugins = {};
4203
+ }
4204
+ resource._eventualConsistencyPlugins[defaultField] = plugin;
4205
+ resource.set = async (id, fieldOrValue, value) => {
4206
+ const hasMultipleFields = Object.keys(resource._eventualConsistencyPlugins).length > 1;
4207
+ if (hasMultipleFields && value === void 0) {
4208
+ throw new Error(`Multiple fields have eventual consistency. Please specify the field: set(id, field, value)`);
4209
+ }
4210
+ const field = value !== void 0 ? fieldOrValue : defaultField;
4211
+ const actualValue = value !== void 0 ? value : fieldOrValue;
4212
+ const fieldPlugin = resource._eventualConsistencyPlugins[field];
4213
+ if (!fieldPlugin) {
4214
+ throw new Error(`No eventual consistency plugin found for field "${field}"`);
4215
+ }
4216
+ await fieldPlugin.createTransaction({
4217
+ originalId: id,
4218
+ operation: "set",
4219
+ value: actualValue,
4220
+ source: "set"
4221
+ });
4222
+ if (fieldPlugin.config.mode === "sync") {
4223
+ const consolidatedValue = await fieldPlugin.consolidateRecord(id);
4224
+ await resource.update(id, {
4225
+ [field]: consolidatedValue
4226
+ });
4227
+ return consolidatedValue;
4228
+ }
4229
+ return actualValue;
4230
+ };
4231
+ resource.add = async (id, fieldOrAmount, amount) => {
4232
+ const hasMultipleFields = Object.keys(resource._eventualConsistencyPlugins).length > 1;
4233
+ if (hasMultipleFields && amount === void 0) {
4234
+ throw new Error(`Multiple fields have eventual consistency. Please specify the field: add(id, field, amount)`);
4235
+ }
4236
+ const field = amount !== void 0 ? fieldOrAmount : defaultField;
4237
+ const actualAmount = amount !== void 0 ? amount : fieldOrAmount;
4238
+ const fieldPlugin = resource._eventualConsistencyPlugins[field];
4239
+ if (!fieldPlugin) {
4240
+ throw new Error(`No eventual consistency plugin found for field "${field}"`);
4241
+ }
4242
+ await fieldPlugin.createTransaction({
4243
+ originalId: id,
4244
+ operation: "add",
4245
+ value: actualAmount,
4246
+ source: "add"
4247
+ });
4248
+ if (fieldPlugin.config.mode === "sync") {
4249
+ const consolidatedValue = await fieldPlugin.consolidateRecord(id);
4250
+ await resource.update(id, {
4251
+ [field]: consolidatedValue
4252
+ });
4253
+ return consolidatedValue;
4254
+ }
4255
+ const currentValue = await fieldPlugin.getConsolidatedValue(id);
4256
+ return currentValue + actualAmount;
4257
+ };
4258
+ resource.sub = async (id, fieldOrAmount, amount) => {
4259
+ const hasMultipleFields = Object.keys(resource._eventualConsistencyPlugins).length > 1;
4260
+ if (hasMultipleFields && amount === void 0) {
4261
+ throw new Error(`Multiple fields have eventual consistency. Please specify the field: sub(id, field, amount)`);
4262
+ }
4263
+ const field = amount !== void 0 ? fieldOrAmount : defaultField;
4264
+ const actualAmount = amount !== void 0 ? amount : fieldOrAmount;
4265
+ const fieldPlugin = resource._eventualConsistencyPlugins[field];
4266
+ if (!fieldPlugin) {
4267
+ throw new Error(`No eventual consistency plugin found for field "${field}"`);
4268
+ }
4269
+ await fieldPlugin.createTransaction({
4270
+ originalId: id,
4271
+ operation: "sub",
4272
+ value: actualAmount,
4273
+ source: "sub"
4274
+ });
4275
+ if (fieldPlugin.config.mode === "sync") {
4276
+ const consolidatedValue = await fieldPlugin.consolidateRecord(id);
4277
+ await resource.update(id, {
4278
+ [field]: consolidatedValue
4279
+ });
4280
+ return consolidatedValue;
4281
+ }
4282
+ const currentValue = await fieldPlugin.getConsolidatedValue(id);
4283
+ return currentValue - actualAmount;
4284
+ };
4285
+ resource.consolidate = async (id, field) => {
4286
+ const hasMultipleFields = Object.keys(resource._eventualConsistencyPlugins).length > 1;
4287
+ if (hasMultipleFields && !field) {
4288
+ throw new Error(`Multiple fields have eventual consistency. Please specify the field: consolidate(id, field)`);
4289
+ }
4290
+ const actualField = field || defaultField;
4291
+ const fieldPlugin = resource._eventualConsistencyPlugins[actualField];
4292
+ if (!fieldPlugin) {
4293
+ throw new Error(`No eventual consistency plugin found for field "${actualField}"`);
4294
+ }
4295
+ return await fieldPlugin.consolidateRecord(id);
4296
+ };
4297
+ resource.getConsolidatedValue = async (id, fieldOrOptions, options) => {
4298
+ if (typeof fieldOrOptions === "string") {
4299
+ const field = fieldOrOptions;
4300
+ const fieldPlugin = resource._eventualConsistencyPlugins[field] || plugin;
4301
+ return await fieldPlugin.getConsolidatedValue(id, options || {});
4302
+ } else {
4303
+ return await plugin.getConsolidatedValue(id, fieldOrOptions || {});
4304
+ }
4305
+ };
4306
+ }
4307
+ async createTransaction(data) {
4308
+ const now = /* @__PURE__ */ new Date();
4309
+ const cohortInfo = this.getCohortInfo(now);
4310
+ const transaction = {
4311
+ id: `txn-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
4312
+ originalId: data.originalId,
4313
+ field: this.config.field,
4314
+ value: data.value || 0,
4315
+ operation: data.operation || "set",
4316
+ timestamp: now.toISOString(),
4317
+ cohortDate: cohortInfo.date,
4318
+ cohortMonth: cohortInfo.month,
4319
+ source: data.source || "unknown",
4320
+ applied: false
4321
+ };
4322
+ if (this.config.batchTransactions) {
4323
+ this.pendingTransactions.set(transaction.id, transaction);
4324
+ if (this.pendingTransactions.size >= this.config.batchSize) {
4325
+ await this.flushPendingTransactions();
4326
+ }
4327
+ } else {
4328
+ await this.transactionResource.insert(transaction);
4329
+ }
4330
+ return transaction;
4331
+ }
4332
+ async flushPendingTransactions() {
4333
+ if (this.pendingTransactions.size === 0) return;
4334
+ const transactions = Array.from(this.pendingTransactions.values());
4335
+ this.pendingTransactions.clear();
4336
+ for (const transaction of transactions) {
4337
+ await this.transactionResource.insert(transaction);
4338
+ }
4339
+ }
4340
+ getCohortInfo(date) {
4341
+ const tz = this.config.cohort.timezone;
4342
+ const offset = this.getTimezoneOffset(tz);
4343
+ const localDate = new Date(date.getTime() + offset);
4344
+ const year = localDate.getFullYear();
4345
+ const month = String(localDate.getMonth() + 1).padStart(2, "0");
4346
+ const day = String(localDate.getDate()).padStart(2, "0");
4347
+ return {
4348
+ date: `${year}-${month}-${day}`,
4349
+ month: `${year}-${month}`
4350
+ };
4351
+ }
4352
+ getTimezoneOffset(timezone) {
4353
+ const offsets = {
4354
+ "UTC": 0,
4355
+ "America/New_York": -5 * 36e5,
4356
+ "America/Chicago": -6 * 36e5,
4357
+ "America/Denver": -7 * 36e5,
4358
+ "America/Los_Angeles": -8 * 36e5,
4359
+ "America/Sao_Paulo": -3 * 36e5,
4360
+ "Europe/London": 0,
4361
+ "Europe/Paris": 1 * 36e5,
4362
+ "Europe/Berlin": 1 * 36e5,
4363
+ "Asia/Tokyo": 9 * 36e5,
4364
+ "Asia/Shanghai": 8 * 36e5,
4365
+ "Australia/Sydney": 10 * 36e5
4366
+ };
4367
+ return offsets[timezone] || 0;
4368
+ }
4369
+ startConsolidationTimer() {
4370
+ const interval = this.config.consolidationInterval;
4371
+ this.consolidationTimer = setInterval(async () => {
4372
+ await this.runConsolidation();
4373
+ }, interval);
4374
+ }
4375
+ async runConsolidation() {
4376
+ try {
4377
+ const [ok, err, transactions] = await tryFn(
4378
+ () => this.transactionResource.query({
4379
+ applied: false
4380
+ })
4381
+ );
4382
+ if (!ok) {
4383
+ console.error("Consolidation failed to query transactions:", err);
4384
+ return;
4385
+ }
4386
+ const uniqueIds = [...new Set(transactions.map((t) => t.originalId))];
4387
+ for (const id of uniqueIds) {
4388
+ await this.consolidateRecord(id);
4389
+ }
4390
+ this.emit("eventual-consistency.consolidated", {
4391
+ resource: this.config.resource,
4392
+ field: this.config.field,
4393
+ recordCount: uniqueIds.length
4394
+ });
4395
+ } catch (error) {
4396
+ console.error("Consolidation error:", error);
4397
+ this.emit("eventual-consistency.consolidation-error", error);
4398
+ }
4399
+ }
4400
+ async consolidateRecord(originalId) {
4401
+ const [recordOk, recordErr, record] = await tryFn(
4402
+ () => this.targetResource.get(originalId)
4403
+ );
4404
+ const currentValue = recordOk && record ? record[this.config.field] || 0 : 0;
4405
+ const [ok, err, transactions] = await tryFn(
4406
+ () => this.transactionResource.query({
4407
+ originalId,
4408
+ applied: false
4409
+ })
4410
+ );
4411
+ if (!ok || !transactions || transactions.length === 0) {
4412
+ return currentValue;
4413
+ }
4414
+ transactions.sort(
4415
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
4416
+ );
4417
+ const hasSetOperation = transactions.some((t) => t.operation === "set");
4418
+ if (currentValue !== 0 && !hasSetOperation) {
4419
+ transactions.unshift({
4420
+ id: "__synthetic__",
4421
+ // Synthetic ID that we'll skip when marking as applied
4422
+ operation: "set",
4423
+ value: currentValue,
4424
+ timestamp: (/* @__PURE__ */ new Date(0)).toISOString()
4425
+ // Very old timestamp to ensure it's first
4426
+ });
4427
+ }
4428
+ const consolidatedValue = this.config.reducer(transactions);
4429
+ const [updateOk, updateErr] = await tryFn(
4430
+ () => this.targetResource.update(originalId, {
4431
+ [this.config.field]: consolidatedValue
4432
+ })
4433
+ );
4434
+ if (updateOk) {
4435
+ for (const txn of transactions) {
4436
+ if (txn.id !== "__synthetic__") {
4437
+ await this.transactionResource.update(txn.id, {
4438
+ applied: true
4439
+ });
4440
+ }
4441
+ }
4442
+ }
4443
+ return consolidatedValue;
4444
+ }
4445
+ async getConsolidatedValue(originalId, options = {}) {
4446
+ const includeApplied = options.includeApplied || false;
4447
+ const startDate = options.startDate;
4448
+ const endDate = options.endDate;
4449
+ const query = { originalId };
4450
+ if (!includeApplied) {
4451
+ query.applied = false;
4452
+ }
4453
+ const [ok, err, transactions] = await tryFn(
4454
+ () => this.transactionResource.query(query)
4455
+ );
4456
+ if (!ok || !transactions || transactions.length === 0) {
4457
+ const [recordOk, recordErr, record] = await tryFn(
4458
+ () => this.targetResource.get(originalId)
4459
+ );
4460
+ if (recordOk && record) {
4461
+ return record[this.config.field] || 0;
4462
+ }
4463
+ return 0;
4464
+ }
4465
+ let filtered = transactions;
4466
+ if (startDate || endDate) {
4467
+ filtered = transactions.filter((t) => {
4468
+ const timestamp = new Date(t.timestamp);
4469
+ if (startDate && timestamp < new Date(startDate)) return false;
4470
+ if (endDate && timestamp > new Date(endDate)) return false;
4471
+ return true;
4472
+ });
4473
+ }
4474
+ filtered.sort(
4475
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
4476
+ );
4477
+ return this.config.reducer(filtered);
4478
+ }
4479
+ // Helper method to get cohort statistics
4480
+ async getCohortStats(cohortDate) {
4481
+ const [ok, err, transactions] = await tryFn(
4482
+ () => this.transactionResource.query({
4483
+ cohortDate
4484
+ })
4485
+ );
4486
+ if (!ok) return null;
4487
+ const stats = {
4488
+ date: cohortDate,
4489
+ transactionCount: transactions.length,
4490
+ totalValue: 0,
4491
+ byOperation: { set: 0, add: 0, sub: 0 },
4492
+ byOriginalId: {}
4493
+ };
4494
+ for (const txn of transactions) {
4495
+ stats.totalValue += txn.value || 0;
4496
+ stats.byOperation[txn.operation] = (stats.byOperation[txn.operation] || 0) + 1;
4497
+ if (!stats.byOriginalId[txn.originalId]) {
4498
+ stats.byOriginalId[txn.originalId] = {
4499
+ count: 0,
4500
+ value: 0
4501
+ };
4502
+ }
4503
+ stats.byOriginalId[txn.originalId].count++;
4504
+ stats.byOriginalId[txn.originalId].value += txn.value || 0;
4505
+ }
4506
+ return stats;
4507
+ }
4508
+ }
4509
+
4053
4510
  class FullTextPlugin extends Plugin {
4054
4511
  constructor(options = {}) {
4055
4512
  super();
@@ -5834,10 +6291,10 @@ class Client extends EventEmitter {
5834
6291
  // Enabled for better performance
5835
6292
  keepAliveMsecs: 1e3,
5836
6293
  // 1 second keep-alive
5837
- maxSockets: 50,
5838
- // Balanced for most applications
5839
- maxFreeSockets: 10,
5840
- // Good connection reuse
6294
+ maxSockets: httpClientOptions.maxSockets || 500,
6295
+ // High concurrency support
6296
+ maxFreeSockets: httpClientOptions.maxFreeSockets || 100,
6297
+ // Better connection reuse
5841
6298
  timeout: 6e4,
5842
6299
  // 60 second timeout
5843
6300
  ...httpClientOptions
@@ -9700,7 +10157,7 @@ class Database extends EventEmitter {
9700
10157
  this.id = idGenerator(7);
9701
10158
  this.version = "1";
9702
10159
  this.s3dbVersion = (() => {
9703
- const [ok, err, version] = tryFn(() => true ? "9.3.0" : "latest");
10160
+ const [ok, err, version] = tryFn(() => true ? "10.0.0" : "latest");
9704
10161
  return ok ? version : "latest";
9705
10162
  })();
9706
10163
  this.resources = {};
@@ -12785,6 +13242,7 @@ exports.Database = Database;
12785
13242
  exports.DatabaseError = DatabaseError;
12786
13243
  exports.EncryptionError = EncryptionError;
12787
13244
  exports.ErrorMap = ErrorMap;
13245
+ exports.EventualConsistencyPlugin = EventualConsistencyPlugin;
12788
13246
  exports.FullTextPlugin = FullTextPlugin;
12789
13247
  exports.InvalidResourceItem = InvalidResourceItem;
12790
13248
  exports.MetricsPlugin = MetricsPlugin;