s3db.js 9.2.2 → 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 +71 -13
- package/dist/s3db.cjs.js +466 -8
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +466 -9
- package/dist/s3db.es.js.map +1 -1
- package/mcp/server.js +12 -8
- package/package.json +4 -4
- package/src/client.class.js +2 -2
- package/src/concerns/high-performance-inserter.js +285 -0
- package/src/concerns/partition-queue.js +171 -0
- package/src/errors.js +10 -2
- package/src/partition-drivers/base-partition-driver.js +96 -0
- package/src/partition-drivers/index.js +60 -0
- package/src/partition-drivers/memory-partition-driver.js +274 -0
- package/src/partition-drivers/sqs-partition-driver.js +332 -0
- package/src/partition-drivers/sync-partition-driver.js +38 -0
- package/src/plugins/backup.plugin.js +1 -1
- package/src/plugins/backup.plugin.js.backup +1 -1
- package/src/plugins/eventual-consistency.plugin.js +609 -0
- package/src/plugins/index.js +1 -0
- package/PLUGINS.md +0 -5036
package/README.md
CHANGED
|
@@ -329,11 +329,65 @@ const s3db = new S3db({
|
|
|
329
329
|
});
|
|
330
330
|
```
|
|
331
331
|
|
|
332
|
-
#### 3.
|
|
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: "
|
|
336
|
-
|
|
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
|
-
|
|
744
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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 || "
|
|
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:
|
|
5838
|
-
//
|
|
5839
|
-
maxFreeSockets:
|
|
5840
|
-
//
|
|
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 ? "
|
|
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;
|