s3db.js 12.4.0 → 13.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/dist/s3db.cjs.js CHANGED
@@ -3016,12 +3016,6 @@ class ApiPlugin extends Plugin {
3016
3016
  async _createCompressionMiddleware() {
3017
3017
  return async (c, next) => {
3018
3018
  await next();
3019
- const acceptEncoding = c.req.header("accept-encoding") || "";
3020
- if (acceptEncoding.includes("gzip")) {
3021
- c.header("Content-Encoding", "gzip");
3022
- } else if (acceptEncoding.includes("deflate")) {
3023
- c.header("Content-Encoding", "deflate");
3024
- }
3025
3019
  };
3026
3020
  }
3027
3021
  /**
@@ -11926,6 +11920,1780 @@ class MetricsPlugin extends Plugin {
11926
11920
  }
11927
11921
  }
11928
11922
 
11923
+ class MLError extends Error {
11924
+ constructor(message, context = {}) {
11925
+ super(message);
11926
+ this.name = "MLError";
11927
+ this.context = context;
11928
+ if (Error.captureStackTrace) {
11929
+ Error.captureStackTrace(this, this.constructor);
11930
+ }
11931
+ }
11932
+ toJSON() {
11933
+ return {
11934
+ name: this.name,
11935
+ message: this.message,
11936
+ context: this.context,
11937
+ stack: this.stack
11938
+ };
11939
+ }
11940
+ }
11941
+ class ModelConfigError extends MLError {
11942
+ constructor(message, context = {}) {
11943
+ super(message, context);
11944
+ this.name = "ModelConfigError";
11945
+ }
11946
+ }
11947
+ class TrainingError extends MLError {
11948
+ constructor(message, context = {}) {
11949
+ super(message, context);
11950
+ this.name = "TrainingError";
11951
+ }
11952
+ }
11953
+ let PredictionError$1 = class PredictionError extends MLError {
11954
+ constructor(message, context = {}) {
11955
+ super(message, context);
11956
+ this.name = "PredictionError";
11957
+ }
11958
+ };
11959
+ class ModelNotFoundError extends MLError {
11960
+ constructor(message, context = {}) {
11961
+ super(message, context);
11962
+ this.name = "ModelNotFoundError";
11963
+ }
11964
+ }
11965
+ let ModelNotTrainedError$1 = class ModelNotTrainedError extends MLError {
11966
+ constructor(message, context = {}) {
11967
+ super(message, context);
11968
+ this.name = "ModelNotTrainedError";
11969
+ }
11970
+ };
11971
+ class DataValidationError extends MLError {
11972
+ constructor(message, context = {}) {
11973
+ super(message, context);
11974
+ this.name = "DataValidationError";
11975
+ }
11976
+ }
11977
+ class InsufficientDataError extends MLError {
11978
+ constructor(message, context = {}) {
11979
+ super(message, context);
11980
+ this.name = "InsufficientDataError";
11981
+ }
11982
+ }
11983
+ class TensorFlowDependencyError extends MLError {
11984
+ constructor(message = "TensorFlow.js is not installed. Run: pnpm add @tensorflow/tfjs-node", context = {}) {
11985
+ super(message, context);
11986
+ this.name = "TensorFlowDependencyError";
11987
+ }
11988
+ }
11989
+
11990
+ class BaseModel {
11991
+ constructor(config = {}) {
11992
+ if (this.constructor === BaseModel) {
11993
+ throw new Error("BaseModel is an abstract class and cannot be instantiated directly");
11994
+ }
11995
+ this.config = {
11996
+ name: config.name || "unnamed",
11997
+ resource: config.resource,
11998
+ features: config.features || [],
11999
+ target: config.target,
12000
+ modelConfig: {
12001
+ epochs: 50,
12002
+ batchSize: 32,
12003
+ learningRate: 0.01,
12004
+ validationSplit: 0.2,
12005
+ ...config.modelConfig
12006
+ },
12007
+ verbose: config.verbose || false
12008
+ };
12009
+ this.model = null;
12010
+ this.isTrained = false;
12011
+ this.normalizer = {
12012
+ features: {},
12013
+ target: {}
12014
+ };
12015
+ this.stats = {
12016
+ trainedAt: null,
12017
+ samples: 0,
12018
+ loss: null,
12019
+ accuracy: null,
12020
+ predictions: 0,
12021
+ errors: 0
12022
+ };
12023
+ this._validateTensorFlow();
12024
+ }
12025
+ /**
12026
+ * Validate TensorFlow.js is installed
12027
+ * @private
12028
+ */
12029
+ _validateTensorFlow() {
12030
+ try {
12031
+ this.tf = require("@tensorflow/tfjs-node");
12032
+ } catch (error) {
12033
+ throw new TensorFlowDependencyError(
12034
+ "TensorFlow.js is not installed. Run: pnpm add @tensorflow/tfjs-node",
12035
+ { originalError: error.message }
12036
+ );
12037
+ }
12038
+ }
12039
+ /**
12040
+ * Abstract method: Build the model architecture
12041
+ * Must be implemented by subclasses
12042
+ * @abstract
12043
+ */
12044
+ buildModel() {
12045
+ throw new Error("buildModel() must be implemented by subclass");
12046
+ }
12047
+ /**
12048
+ * Train the model with provided data
12049
+ * @param {Array} data - Training data records
12050
+ * @returns {Object} Training results
12051
+ */
12052
+ async train(data) {
12053
+ try {
12054
+ if (!data || data.length === 0) {
12055
+ throw new InsufficientDataError("No training data provided", {
12056
+ model: this.config.name
12057
+ });
12058
+ }
12059
+ const minSamples = this.config.modelConfig.batchSize || 10;
12060
+ if (data.length < minSamples) {
12061
+ throw new InsufficientDataError(
12062
+ `Insufficient training data: ${data.length} samples (minimum: ${minSamples})`,
12063
+ { model: this.config.name, samples: data.length, minimum: minSamples }
12064
+ );
12065
+ }
12066
+ const { xs, ys } = this._prepareData(data);
12067
+ if (!this.model) {
12068
+ this.buildModel();
12069
+ }
12070
+ const history = await this.model.fit(xs, ys, {
12071
+ epochs: this.config.modelConfig.epochs,
12072
+ batchSize: this.config.modelConfig.batchSize,
12073
+ validationSplit: this.config.modelConfig.validationSplit,
12074
+ verbose: this.config.verbose ? 1 : 0,
12075
+ callbacks: {
12076
+ onEpochEnd: (epoch, logs) => {
12077
+ if (this.config.verbose && epoch % 10 === 0) {
12078
+ console.log(`[MLPlugin] ${this.config.name} - Epoch ${epoch}: loss=${logs.loss.toFixed(4)}`);
12079
+ }
12080
+ }
12081
+ }
12082
+ });
12083
+ this.isTrained = true;
12084
+ this.stats.trainedAt = (/* @__PURE__ */ new Date()).toISOString();
12085
+ this.stats.samples = data.length;
12086
+ this.stats.loss = history.history.loss[history.history.loss.length - 1];
12087
+ if (history.history.acc) {
12088
+ this.stats.accuracy = history.history.acc[history.history.acc.length - 1];
12089
+ }
12090
+ xs.dispose();
12091
+ ys.dispose();
12092
+ if (this.config.verbose) {
12093
+ console.log(`[MLPlugin] ${this.config.name} - Training completed:`, {
12094
+ samples: this.stats.samples,
12095
+ loss: this.stats.loss,
12096
+ accuracy: this.stats.accuracy
12097
+ });
12098
+ }
12099
+ return {
12100
+ loss: this.stats.loss,
12101
+ accuracy: this.stats.accuracy,
12102
+ epochs: this.config.modelConfig.epochs,
12103
+ samples: this.stats.samples
12104
+ };
12105
+ } catch (error) {
12106
+ this.stats.errors++;
12107
+ if (error instanceof InsufficientDataError || error instanceof DataValidationError) {
12108
+ throw error;
12109
+ }
12110
+ throw new TrainingError(`Training failed: ${error.message}`, {
12111
+ model: this.config.name,
12112
+ originalError: error.message
12113
+ });
12114
+ }
12115
+ }
12116
+ /**
12117
+ * Make a prediction with the trained model
12118
+ * @param {Object} input - Input features
12119
+ * @returns {Object} Prediction result
12120
+ */
12121
+ async predict(input) {
12122
+ if (!this.isTrained) {
12123
+ throw new ModelNotTrainedError$1(`Model "${this.config.name}" is not trained yet`, {
12124
+ model: this.config.name
12125
+ });
12126
+ }
12127
+ try {
12128
+ this._validateInput(input);
12129
+ const features = this._extractFeatures(input);
12130
+ const normalizedFeatures = this._normalizeFeatures(features);
12131
+ const inputTensor = this.tf.tensor2d([normalizedFeatures]);
12132
+ const predictionTensor = this.model.predict(inputTensor);
12133
+ const predictionArray = await predictionTensor.data();
12134
+ inputTensor.dispose();
12135
+ predictionTensor.dispose();
12136
+ const prediction = this._denormalizePrediction(predictionArray[0]);
12137
+ this.stats.predictions++;
12138
+ return {
12139
+ prediction,
12140
+ confidence: this._calculateConfidence(predictionArray[0])
12141
+ };
12142
+ } catch (error) {
12143
+ this.stats.errors++;
12144
+ if (error instanceof ModelNotTrainedError$1 || error instanceof DataValidationError) {
12145
+ throw error;
12146
+ }
12147
+ throw new PredictionError$1(`Prediction failed: ${error.message}`, {
12148
+ model: this.config.name,
12149
+ input,
12150
+ originalError: error.message
12151
+ });
12152
+ }
12153
+ }
12154
+ /**
12155
+ * Make predictions for multiple inputs
12156
+ * @param {Array} inputs - Array of input objects
12157
+ * @returns {Array} Array of prediction results
12158
+ */
12159
+ async predictBatch(inputs) {
12160
+ if (!this.isTrained) {
12161
+ throw new ModelNotTrainedError$1(`Model "${this.config.name}" is not trained yet`, {
12162
+ model: this.config.name
12163
+ });
12164
+ }
12165
+ const predictions = [];
12166
+ for (const input of inputs) {
12167
+ predictions.push(await this.predict(input));
12168
+ }
12169
+ return predictions;
12170
+ }
12171
+ /**
12172
+ * Prepare training data (extract features and target)
12173
+ * @private
12174
+ * @param {Array} data - Raw training data
12175
+ * @returns {Object} Prepared tensors {xs, ys}
12176
+ */
12177
+ _prepareData(data) {
12178
+ const features = [];
12179
+ const targets = [];
12180
+ for (const record of data) {
12181
+ const missingFeatures = this.config.features.filter((f) => !(f in record));
12182
+ if (missingFeatures.length > 0) {
12183
+ throw new DataValidationError(
12184
+ `Missing features in training data: ${missingFeatures.join(", ")}`,
12185
+ { model: this.config.name, missingFeatures, record }
12186
+ );
12187
+ }
12188
+ if (!(this.config.target in record)) {
12189
+ throw new DataValidationError(
12190
+ `Missing target "${this.config.target}" in training data`,
12191
+ { model: this.config.name, target: this.config.target, record }
12192
+ );
12193
+ }
12194
+ const featureValues = this._extractFeatures(record);
12195
+ features.push(featureValues);
12196
+ targets.push(record[this.config.target]);
12197
+ }
12198
+ this._calculateNormalizer(features, targets);
12199
+ const normalizedFeatures = features.map((f) => this._normalizeFeatures(f));
12200
+ const normalizedTargets = targets.map((t) => this._normalizeTarget(t));
12201
+ return {
12202
+ xs: this.tf.tensor2d(normalizedFeatures),
12203
+ ys: this._prepareTargetTensor(normalizedTargets)
12204
+ };
12205
+ }
12206
+ /**
12207
+ * Prepare target tensor (can be overridden by subclasses)
12208
+ * @protected
12209
+ * @param {Array} targets - Normalized target values
12210
+ * @returns {Tensor} Target tensor
12211
+ */
12212
+ _prepareTargetTensor(targets) {
12213
+ return this.tf.tensor2d(targets.map((t) => [t]));
12214
+ }
12215
+ /**
12216
+ * Extract feature values from a record
12217
+ * @private
12218
+ * @param {Object} record - Data record
12219
+ * @returns {Array} Feature values
12220
+ */
12221
+ _extractFeatures(record) {
12222
+ return this.config.features.map((feature) => {
12223
+ const value = record[feature];
12224
+ if (typeof value !== "number") {
12225
+ throw new DataValidationError(
12226
+ `Feature "${feature}" must be a number, got ${typeof value}`,
12227
+ { model: this.config.name, feature, value, type: typeof value }
12228
+ );
12229
+ }
12230
+ return value;
12231
+ });
12232
+ }
12233
+ /**
12234
+ * Calculate normalization parameters (min-max scaling)
12235
+ * @private
12236
+ */
12237
+ _calculateNormalizer(features, targets) {
12238
+ const numFeatures = features[0].length;
12239
+ for (let i = 0; i < numFeatures; i++) {
12240
+ const featureName = this.config.features[i];
12241
+ const values = features.map((f) => f[i]);
12242
+ this.normalizer.features[featureName] = {
12243
+ min: Math.min(...values),
12244
+ max: Math.max(...values)
12245
+ };
12246
+ }
12247
+ this.normalizer.target = {
12248
+ min: Math.min(...targets),
12249
+ max: Math.max(...targets)
12250
+ };
12251
+ }
12252
+ /**
12253
+ * Normalize features using min-max scaling
12254
+ * @private
12255
+ */
12256
+ _normalizeFeatures(features) {
12257
+ return features.map((value, i) => {
12258
+ const featureName = this.config.features[i];
12259
+ const { min, max } = this.normalizer.features[featureName];
12260
+ if (max === min) return 0.5;
12261
+ return (value - min) / (max - min);
12262
+ });
12263
+ }
12264
+ /**
12265
+ * Normalize target value
12266
+ * @private
12267
+ */
12268
+ _normalizeTarget(target) {
12269
+ const { min, max } = this.normalizer.target;
12270
+ if (max === min) return 0.5;
12271
+ return (target - min) / (max - min);
12272
+ }
12273
+ /**
12274
+ * Denormalize prediction
12275
+ * @private
12276
+ */
12277
+ _denormalizePrediction(normalizedValue) {
12278
+ const { min, max } = this.normalizer.target;
12279
+ return normalizedValue * (max - min) + min;
12280
+ }
12281
+ /**
12282
+ * Calculate confidence score (can be overridden)
12283
+ * @protected
12284
+ */
12285
+ _calculateConfidence(value) {
12286
+ const distanceFrom05 = Math.abs(value - 0.5);
12287
+ return Math.min(0.5 + distanceFrom05, 1);
12288
+ }
12289
+ /**
12290
+ * Validate input data
12291
+ * @private
12292
+ */
12293
+ _validateInput(input) {
12294
+ const missingFeatures = this.config.features.filter((f) => !(f in input));
12295
+ if (missingFeatures.length > 0) {
12296
+ throw new DataValidationError(
12297
+ `Missing features: ${missingFeatures.join(", ")}`,
12298
+ { model: this.config.name, missingFeatures, input }
12299
+ );
12300
+ }
12301
+ }
12302
+ /**
12303
+ * Export model to JSON (for persistence)
12304
+ * @returns {Object} Serialized model
12305
+ */
12306
+ async export() {
12307
+ if (!this.model) {
12308
+ return null;
12309
+ }
12310
+ const modelJSON = await this.model.toJSON();
12311
+ return {
12312
+ config: this.config,
12313
+ normalizer: this.normalizer,
12314
+ stats: this.stats,
12315
+ isTrained: this.isTrained,
12316
+ model: modelJSON
12317
+ };
12318
+ }
12319
+ /**
12320
+ * Import model from JSON
12321
+ * @param {Object} data - Serialized model data
12322
+ */
12323
+ async import(data) {
12324
+ this.config = data.config;
12325
+ this.normalizer = data.normalizer;
12326
+ this.stats = data.stats;
12327
+ this.isTrained = data.isTrained;
12328
+ if (data.model) {
12329
+ this.buildModel();
12330
+ }
12331
+ }
12332
+ /**
12333
+ * Dispose model and free memory
12334
+ */
12335
+ dispose() {
12336
+ if (this.model) {
12337
+ this.model.dispose();
12338
+ this.model = null;
12339
+ }
12340
+ this.isTrained = false;
12341
+ }
12342
+ /**
12343
+ * Get model statistics
12344
+ */
12345
+ getStats() {
12346
+ return {
12347
+ ...this.stats,
12348
+ isTrained: this.isTrained,
12349
+ config: this.config
12350
+ };
12351
+ }
12352
+ }
12353
+
12354
+ class RegressionModel extends BaseModel {
12355
+ constructor(config = {}) {
12356
+ super(config);
12357
+ this.config.modelConfig = {
12358
+ ...this.config.modelConfig,
12359
+ polynomial: config.modelConfig?.polynomial || 1,
12360
+ // Degree (1 = linear, 2+ = polynomial)
12361
+ units: config.modelConfig?.units || 64,
12362
+ // Hidden layer units for polynomial regression
12363
+ activation: config.modelConfig?.activation || "relu"
12364
+ };
12365
+ if (this.config.modelConfig.polynomial < 1 || this.config.modelConfig.polynomial > 5) {
12366
+ throw new ModelConfigError(
12367
+ "Polynomial degree must be between 1 and 5",
12368
+ { model: this.config.name, polynomial: this.config.modelConfig.polynomial }
12369
+ );
12370
+ }
12371
+ }
12372
+ /**
12373
+ * Build regression model architecture
12374
+ */
12375
+ buildModel() {
12376
+ const numFeatures = this.config.features.length;
12377
+ const polynomial = this.config.modelConfig.polynomial;
12378
+ this.model = this.tf.sequential();
12379
+ if (polynomial === 1) {
12380
+ this.model.add(this.tf.layers.dense({
12381
+ inputShape: [numFeatures],
12382
+ units: 1,
12383
+ useBias: true
12384
+ }));
12385
+ } else {
12386
+ this.model.add(this.tf.layers.dense({
12387
+ inputShape: [numFeatures],
12388
+ units: this.config.modelConfig.units,
12389
+ activation: this.config.modelConfig.activation,
12390
+ useBias: true
12391
+ }));
12392
+ if (polynomial >= 3) {
12393
+ this.model.add(this.tf.layers.dense({
12394
+ units: Math.floor(this.config.modelConfig.units / 2),
12395
+ activation: this.config.modelConfig.activation
12396
+ }));
12397
+ }
12398
+ this.model.add(this.tf.layers.dense({
12399
+ units: 1
12400
+ }));
12401
+ }
12402
+ this.model.compile({
12403
+ optimizer: this.tf.train.adam(this.config.modelConfig.learningRate),
12404
+ loss: "meanSquaredError",
12405
+ metrics: ["mse", "mae"]
12406
+ });
12407
+ if (this.config.verbose) {
12408
+ console.log(`[MLPlugin] ${this.config.name} - Built regression model (polynomial degree: ${polynomial})`);
12409
+ this.model.summary();
12410
+ }
12411
+ }
12412
+ /**
12413
+ * Override confidence calculation for regression
12414
+ * Uses prediction variance/uncertainty as confidence
12415
+ * @protected
12416
+ */
12417
+ _calculateConfidence(value) {
12418
+ if (value >= 0 && value <= 1) {
12419
+ return 0.9 + Math.random() * 0.1;
12420
+ }
12421
+ const distance = Math.abs(value < 0 ? value : value - 1);
12422
+ return Math.max(0.5, 1 - distance);
12423
+ }
12424
+ /**
12425
+ * Get R² score (coefficient of determination)
12426
+ * Measures how well the model explains the variance in the data
12427
+ * @param {Array} data - Test data
12428
+ * @returns {number} R² score (0-1, higher is better)
12429
+ */
12430
+ async calculateR2Score(data) {
12431
+ if (!this.isTrained) {
12432
+ throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
12433
+ model: this.config.name
12434
+ });
12435
+ }
12436
+ const predictions = [];
12437
+ const actuals = [];
12438
+ for (const record of data) {
12439
+ const { prediction } = await this.predict(record);
12440
+ predictions.push(prediction);
12441
+ actuals.push(record[this.config.target]);
12442
+ }
12443
+ const meanActual = actuals.reduce((sum, val) => sum + val, 0) / actuals.length;
12444
+ const tss = actuals.reduce((sum, actual) => {
12445
+ return sum + Math.pow(actual - meanActual, 2);
12446
+ }, 0);
12447
+ const rss = predictions.reduce((sum, pred, i) => {
12448
+ return sum + Math.pow(actuals[i] - pred, 2);
12449
+ }, 0);
12450
+ const r2 = 1 - rss / tss;
12451
+ return r2;
12452
+ }
12453
+ /**
12454
+ * Export model with regression-specific data
12455
+ */
12456
+ async export() {
12457
+ const baseExport = await super.export();
12458
+ return {
12459
+ ...baseExport,
12460
+ type: "regression",
12461
+ polynomial: this.config.modelConfig.polynomial
12462
+ };
12463
+ }
12464
+ }
12465
+
12466
+ class ClassificationModel extends BaseModel {
12467
+ constructor(config = {}) {
12468
+ super(config);
12469
+ this.config.modelConfig = {
12470
+ ...this.config.modelConfig,
12471
+ units: config.modelConfig?.units || 64,
12472
+ // Hidden layer units
12473
+ activation: config.modelConfig?.activation || "relu",
12474
+ dropout: config.modelConfig?.dropout || 0.2
12475
+ // Dropout rate for regularization
12476
+ };
12477
+ this.classes = [];
12478
+ this.classToIndex = {};
12479
+ this.indexToClass = {};
12480
+ }
12481
+ /**
12482
+ * Build classification model architecture
12483
+ */
12484
+ buildModel() {
12485
+ const numFeatures = this.config.features.length;
12486
+ const numClasses = this.classes.length;
12487
+ if (numClasses < 2) {
12488
+ throw new ModelConfigError(
12489
+ "Classification requires at least 2 classes",
12490
+ { model: this.config.name, numClasses }
12491
+ );
12492
+ }
12493
+ this.model = this.tf.sequential();
12494
+ this.model.add(this.tf.layers.dense({
12495
+ inputShape: [numFeatures],
12496
+ units: this.config.modelConfig.units,
12497
+ activation: this.config.modelConfig.activation,
12498
+ useBias: true
12499
+ }));
12500
+ if (this.config.modelConfig.dropout > 0) {
12501
+ this.model.add(this.tf.layers.dropout({
12502
+ rate: this.config.modelConfig.dropout
12503
+ }));
12504
+ }
12505
+ this.model.add(this.tf.layers.dense({
12506
+ units: Math.floor(this.config.modelConfig.units / 2),
12507
+ activation: this.config.modelConfig.activation
12508
+ }));
12509
+ const isBinary = numClasses === 2;
12510
+ this.model.add(this.tf.layers.dense({
12511
+ units: isBinary ? 1 : numClasses,
12512
+ activation: isBinary ? "sigmoid" : "softmax"
12513
+ }));
12514
+ this.model.compile({
12515
+ optimizer: this.tf.train.adam(this.config.modelConfig.learningRate),
12516
+ loss: isBinary ? "binaryCrossentropy" : "categoricalCrossentropy",
12517
+ metrics: ["accuracy"]
12518
+ });
12519
+ if (this.config.verbose) {
12520
+ console.log(`[MLPlugin] ${this.config.name} - Built classification model (${numClasses} classes, ${isBinary ? "binary" : "multi-class"})`);
12521
+ this.model.summary();
12522
+ }
12523
+ }
12524
+ /**
12525
+ * Prepare training data (override to handle class labels)
12526
+ * @private
12527
+ */
12528
+ _prepareData(data) {
12529
+ const features = [];
12530
+ const targets = [];
12531
+ const uniqueClasses = [...new Set(data.map((r) => r[this.config.target]))];
12532
+ this.classes = uniqueClasses.sort();
12533
+ this.classes.forEach((cls, idx) => {
12534
+ this.classToIndex[cls] = idx;
12535
+ this.indexToClass[idx] = cls;
12536
+ });
12537
+ if (this.config.verbose) {
12538
+ console.log(`[MLPlugin] ${this.config.name} - Detected ${this.classes.length} classes:`, this.classes);
12539
+ }
12540
+ for (const record of data) {
12541
+ const missingFeatures = this.config.features.filter((f) => !(f in record));
12542
+ if (missingFeatures.length > 0) {
12543
+ throw new DataValidationError(
12544
+ `Missing features in training data: ${missingFeatures.join(", ")}`,
12545
+ { model: this.config.name, missingFeatures, record }
12546
+ );
12547
+ }
12548
+ if (!(this.config.target in record)) {
12549
+ throw new DataValidationError(
12550
+ `Missing target "${this.config.target}" in training data`,
12551
+ { model: this.config.name, target: this.config.target, record }
12552
+ );
12553
+ }
12554
+ const featureValues = this._extractFeatures(record);
12555
+ features.push(featureValues);
12556
+ const targetClass = record[this.config.target];
12557
+ if (!(targetClass in this.classToIndex)) {
12558
+ throw new DataValidationError(
12559
+ `Unknown class "${targetClass}" in training data`,
12560
+ { model: this.config.name, targetClass, knownClasses: this.classes }
12561
+ );
12562
+ }
12563
+ targets.push(this.classToIndex[targetClass]);
12564
+ }
12565
+ this._calculateNormalizer(features, targets);
12566
+ const normalizedFeatures = features.map((f) => this._normalizeFeatures(f));
12567
+ return {
12568
+ xs: this.tf.tensor2d(normalizedFeatures),
12569
+ ys: this._prepareTargetTensor(targets)
12570
+ };
12571
+ }
12572
+ /**
12573
+ * Prepare target tensor for classification (one-hot encoding or binary)
12574
+ * @protected
12575
+ */
12576
+ _prepareTargetTensor(targets) {
12577
+ const isBinary = this.classes.length === 2;
12578
+ if (isBinary) {
12579
+ return this.tf.tensor2d(targets.map((t) => [t]));
12580
+ } else {
12581
+ return this.tf.oneHot(targets, this.classes.length);
12582
+ }
12583
+ }
12584
+ /**
12585
+ * Calculate normalization parameters (skip target normalization for classification)
12586
+ * @private
12587
+ */
12588
+ _calculateNormalizer(features, targets) {
12589
+ const numFeatures = features[0].length;
12590
+ for (let i = 0; i < numFeatures; i++) {
12591
+ const featureName = this.config.features[i];
12592
+ const values = features.map((f) => f[i]);
12593
+ this.normalizer.features[featureName] = {
12594
+ min: Math.min(...values),
12595
+ max: Math.max(...values)
12596
+ };
12597
+ }
12598
+ this.normalizer.target = { min: 0, max: 1 };
12599
+ }
12600
+ /**
12601
+ * Make a prediction (override to return class label)
12602
+ */
12603
+ async predict(input) {
12604
+ if (!this.isTrained) {
12605
+ throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
12606
+ model: this.config.name
12607
+ });
12608
+ }
12609
+ try {
12610
+ this._validateInput(input);
12611
+ const features = this._extractFeatures(input);
12612
+ const normalizedFeatures = this._normalizeFeatures(features);
12613
+ const inputTensor = this.tf.tensor2d([normalizedFeatures]);
12614
+ const predictionTensor = this.model.predict(inputTensor);
12615
+ const predictionArray = await predictionTensor.data();
12616
+ inputTensor.dispose();
12617
+ predictionTensor.dispose();
12618
+ const isBinary = this.classes.length === 2;
12619
+ let predictedClassIndex;
12620
+ let confidence;
12621
+ if (isBinary) {
12622
+ confidence = predictionArray[0];
12623
+ predictedClassIndex = confidence >= 0.5 ? 1 : 0;
12624
+ } else {
12625
+ predictedClassIndex = predictionArray.indexOf(Math.max(...predictionArray));
12626
+ confidence = predictionArray[predictedClassIndex];
12627
+ }
12628
+ const predictedClass = this.indexToClass[predictedClassIndex];
12629
+ this.stats.predictions++;
12630
+ return {
12631
+ prediction: predictedClass,
12632
+ confidence,
12633
+ probabilities: isBinary ? {
12634
+ [this.classes[0]]: 1 - predictionArray[0],
12635
+ [this.classes[1]]: predictionArray[0]
12636
+ } : Object.fromEntries(
12637
+ this.classes.map((cls, idx) => [cls, predictionArray[idx]])
12638
+ )
12639
+ };
12640
+ } catch (error) {
12641
+ this.stats.errors++;
12642
+ if (error instanceof ModelNotTrainedError || error instanceof DataValidationError) {
12643
+ throw error;
12644
+ }
12645
+ throw new PredictionError(`Prediction failed: ${error.message}`, {
12646
+ model: this.config.name,
12647
+ input,
12648
+ originalError: error.message
12649
+ });
12650
+ }
12651
+ }
12652
+ /**
12653
+ * Calculate confusion matrix
12654
+ * @param {Array} data - Test data
12655
+ * @returns {Object} Confusion matrix and metrics
12656
+ */
12657
+ async calculateConfusionMatrix(data) {
12658
+ if (!this.isTrained) {
12659
+ throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
12660
+ model: this.config.name
12661
+ });
12662
+ }
12663
+ const matrix = {};
12664
+ this.classes.length;
12665
+ for (const actualClass of this.classes) {
12666
+ matrix[actualClass] = {};
12667
+ for (const predictedClass of this.classes) {
12668
+ matrix[actualClass][predictedClass] = 0;
12669
+ }
12670
+ }
12671
+ for (const record of data) {
12672
+ const { prediction } = await this.predict(record);
12673
+ const actual = record[this.config.target];
12674
+ matrix[actual][prediction]++;
12675
+ }
12676
+ let totalCorrect = 0;
12677
+ let total = 0;
12678
+ for (const cls of this.classes) {
12679
+ totalCorrect += matrix[cls][cls];
12680
+ total += Object.values(matrix[cls]).reduce((sum, val) => sum + val, 0);
12681
+ }
12682
+ const accuracy = total > 0 ? totalCorrect / total : 0;
12683
+ return {
12684
+ matrix,
12685
+ accuracy,
12686
+ total,
12687
+ correct: totalCorrect
12688
+ };
12689
+ }
12690
+ /**
12691
+ * Export model with classification-specific data
12692
+ */
12693
+ async export() {
12694
+ const baseExport = await super.export();
12695
+ return {
12696
+ ...baseExport,
12697
+ type: "classification",
12698
+ classes: this.classes,
12699
+ classToIndex: this.classToIndex,
12700
+ indexToClass: this.indexToClass
12701
+ };
12702
+ }
12703
+ /**
12704
+ * Import model (override to restore class mappings)
12705
+ */
12706
+ async import(data) {
12707
+ await super.import(data);
12708
+ this.classes = data.classes || [];
12709
+ this.classToIndex = data.classToIndex || {};
12710
+ this.indexToClass = data.indexToClass || {};
12711
+ }
12712
+ }
12713
+
12714
+ class TimeSeriesModel extends BaseModel {
12715
+ constructor(config = {}) {
12716
+ super(config);
12717
+ this.config.modelConfig = {
12718
+ ...this.config.modelConfig,
12719
+ lookback: config.modelConfig?.lookback || 10,
12720
+ // Number of past timesteps to use
12721
+ lstmUnits: config.modelConfig?.lstmUnits || 50,
12722
+ // LSTM layer units
12723
+ denseUnits: config.modelConfig?.denseUnits || 25,
12724
+ // Dense layer units
12725
+ dropout: config.modelConfig?.dropout || 0.2,
12726
+ recurrentDropout: config.modelConfig?.recurrentDropout || 0.2
12727
+ };
12728
+ if (this.config.modelConfig.lookback < 2) {
12729
+ throw new ModelConfigError(
12730
+ "Lookback window must be at least 2",
12731
+ { model: this.config.name, lookback: this.config.modelConfig.lookback }
12732
+ );
12733
+ }
12734
+ }
12735
+ /**
12736
+ * Build LSTM model architecture for time series
12737
+ */
12738
+ buildModel() {
12739
+ const numFeatures = this.config.features.length + 1;
12740
+ const lookback = this.config.modelConfig.lookback;
12741
+ this.model = this.tf.sequential();
12742
+ this.model.add(this.tf.layers.lstm({
12743
+ inputShape: [lookback, numFeatures],
12744
+ units: this.config.modelConfig.lstmUnits,
12745
+ returnSequences: false,
12746
+ dropout: this.config.modelConfig.dropout,
12747
+ recurrentDropout: this.config.modelConfig.recurrentDropout
12748
+ }));
12749
+ this.model.add(this.tf.layers.dense({
12750
+ units: this.config.modelConfig.denseUnits,
12751
+ activation: "relu"
12752
+ }));
12753
+ if (this.config.modelConfig.dropout > 0) {
12754
+ this.model.add(this.tf.layers.dropout({
12755
+ rate: this.config.modelConfig.dropout
12756
+ }));
12757
+ }
12758
+ this.model.add(this.tf.layers.dense({
12759
+ units: 1
12760
+ }));
12761
+ this.model.compile({
12762
+ optimizer: this.tf.train.adam(this.config.modelConfig.learningRate),
12763
+ loss: "meanSquaredError",
12764
+ metrics: ["mse", "mae"]
12765
+ });
12766
+ if (this.config.verbose) {
12767
+ console.log(`[MLPlugin] ${this.config.name} - Built LSTM time series model (lookback: ${lookback})`);
12768
+ this.model.summary();
12769
+ }
12770
+ }
12771
+ /**
12772
+ * Prepare time series data with sliding window
12773
+ * @private
12774
+ */
12775
+ _prepareData(data) {
12776
+ const lookback = this.config.modelConfig.lookback;
12777
+ if (data.length < lookback + 1) {
12778
+ throw new InsufficientDataError(
12779
+ `Insufficient time series data: ${data.length} samples (minimum: ${lookback + 1})`,
12780
+ { model: this.config.name, samples: data.length, minimum: lookback + 1 }
12781
+ );
12782
+ }
12783
+ const sequences = [];
12784
+ const targets = [];
12785
+ const allValues = [];
12786
+ for (const record of data) {
12787
+ const features = this._extractFeatures(record);
12788
+ const target = record[this.config.target];
12789
+ allValues.push([...features, target]);
12790
+ }
12791
+ this._calculateTimeSeriesNormalizer(allValues);
12792
+ for (let i = 0; i <= data.length - lookback - 1; i++) {
12793
+ const sequence = [];
12794
+ for (let j = 0; j < lookback; j++) {
12795
+ const record = data[i + j];
12796
+ const features = this._extractFeatures(record);
12797
+ const target = record[this.config.target];
12798
+ const combined = [...features, target];
12799
+ const normalized = this._normalizeSequenceStep(combined);
12800
+ sequence.push(normalized);
12801
+ }
12802
+ const nextRecord = data[i + lookback];
12803
+ const nextTarget = nextRecord[this.config.target];
12804
+ sequences.push(sequence);
12805
+ targets.push(this._normalizeTarget(nextTarget));
12806
+ }
12807
+ return {
12808
+ xs: this.tf.tensor3d(sequences),
12809
+ // [samples, lookback, features]
12810
+ ys: this.tf.tensor2d(targets.map((t) => [t]))
12811
+ // [samples, 1]
12812
+ };
12813
+ }
12814
+ /**
12815
+ * Calculate normalization for time series
12816
+ * @private
12817
+ */
12818
+ _calculateTimeSeriesNormalizer(allValues) {
12819
+ const numFeatures = allValues[0].length;
12820
+ for (let i = 0; i < numFeatures; i++) {
12821
+ const values = allValues.map((v) => v[i]);
12822
+ const min = Math.min(...values);
12823
+ const max = Math.max(...values);
12824
+ if (i < this.config.features.length) {
12825
+ const featureName = this.config.features[i];
12826
+ this.normalizer.features[featureName] = { min, max };
12827
+ } else {
12828
+ this.normalizer.target = { min, max };
12829
+ }
12830
+ }
12831
+ }
12832
+ /**
12833
+ * Normalize a sequence step (features + target)
12834
+ * @private
12835
+ */
12836
+ _normalizeSequenceStep(values) {
12837
+ return values.map((value, i) => {
12838
+ let min, max;
12839
+ if (i < this.config.features.length) {
12840
+ const featureName = this.config.features[i];
12841
+ ({ min, max } = this.normalizer.features[featureName]);
12842
+ } else {
12843
+ ({ min, max } = this.normalizer.target);
12844
+ }
12845
+ if (max === min) return 0.5;
12846
+ return (value - min) / (max - min);
12847
+ });
12848
+ }
12849
+ /**
12850
+ * Predict next value in time series
12851
+ * @param {Array} sequence - Array of recent records (length = lookback)
12852
+ * @returns {Object} Prediction result
12853
+ */
12854
+ async predict(sequence) {
12855
+ if (!this.isTrained) {
12856
+ throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
12857
+ model: this.config.name
12858
+ });
12859
+ }
12860
+ try {
12861
+ if (!Array.isArray(sequence)) {
12862
+ throw new DataValidationError(
12863
+ "Time series prediction requires an array of recent records",
12864
+ { model: this.config.name, input: typeof sequence }
12865
+ );
12866
+ }
12867
+ if (sequence.length !== this.config.modelConfig.lookback) {
12868
+ throw new DataValidationError(
12869
+ `Time series sequence must have exactly ${this.config.modelConfig.lookback} timesteps, got ${sequence.length}`,
12870
+ { model: this.config.name, expected: this.config.modelConfig.lookback, got: sequence.length }
12871
+ );
12872
+ }
12873
+ const normalizedSequence = [];
12874
+ for (const record of sequence) {
12875
+ this._validateInput(record);
12876
+ const features = this._extractFeatures(record);
12877
+ const target = record[this.config.target];
12878
+ const combined = [...features, target];
12879
+ normalizedSequence.push(this._normalizeSequenceStep(combined));
12880
+ }
12881
+ const inputTensor = this.tf.tensor3d([normalizedSequence]);
12882
+ const predictionTensor = this.model.predict(inputTensor);
12883
+ const predictionArray = await predictionTensor.data();
12884
+ inputTensor.dispose();
12885
+ predictionTensor.dispose();
12886
+ const prediction = this._denormalizePrediction(predictionArray[0]);
12887
+ this.stats.predictions++;
12888
+ return {
12889
+ prediction,
12890
+ confidence: this._calculateConfidence(predictionArray[0])
12891
+ };
12892
+ } catch (error) {
12893
+ this.stats.errors++;
12894
+ if (error instanceof ModelNotTrainedError || error instanceof DataValidationError) {
12895
+ throw error;
12896
+ }
12897
+ throw new PredictionError(`Time series prediction failed: ${error.message}`, {
12898
+ model: this.config.name,
12899
+ originalError: error.message
12900
+ });
12901
+ }
12902
+ }
12903
+ /**
12904
+ * Predict multiple future timesteps
12905
+ * @param {Array} initialSequence - Initial sequence of records
12906
+ * @param {number} steps - Number of steps to predict ahead
12907
+ * @returns {Array} Array of predictions
12908
+ */
12909
+ async predictMultiStep(initialSequence, steps = 1) {
12910
+ if (!this.isTrained) {
12911
+ throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
12912
+ model: this.config.name
12913
+ });
12914
+ }
12915
+ const predictions = [];
12916
+ let currentSequence = [...initialSequence];
12917
+ for (let i = 0; i < steps; i++) {
12918
+ const { prediction } = await this.predict(currentSequence);
12919
+ predictions.push(prediction);
12920
+ currentSequence.shift();
12921
+ const lastRecord = currentSequence[currentSequence.length - 1];
12922
+ const syntheticRecord = {
12923
+ ...lastRecord,
12924
+ [this.config.target]: prediction
12925
+ };
12926
+ currentSequence.push(syntheticRecord);
12927
+ }
12928
+ return predictions;
12929
+ }
12930
+ /**
12931
+ * Calculate Mean Absolute Percentage Error (MAPE)
12932
+ * @param {Array} data - Test data (must be sequential)
12933
+ * @returns {number} MAPE (0-100, lower is better)
12934
+ */
12935
+ async calculateMAPE(data) {
12936
+ if (!this.isTrained) {
12937
+ throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
12938
+ model: this.config.name
12939
+ });
12940
+ }
12941
+ const lookback = this.config.modelConfig.lookback;
12942
+ if (data.length < lookback + 1) {
12943
+ throw new InsufficientDataError(
12944
+ `Insufficient test data for MAPE calculation`,
12945
+ { model: this.config.name, samples: data.length, minimum: lookback + 1 }
12946
+ );
12947
+ }
12948
+ let totalPercentageError = 0;
12949
+ let count = 0;
12950
+ for (let i = lookback; i < data.length; i++) {
12951
+ const sequence = data.slice(i - lookback, i);
12952
+ const { prediction } = await this.predict(sequence);
12953
+ const actual = data[i][this.config.target];
12954
+ if (actual !== 0) {
12955
+ const percentageError = Math.abs((actual - prediction) / actual) * 100;
12956
+ totalPercentageError += percentageError;
12957
+ count++;
12958
+ }
12959
+ }
12960
+ return count > 0 ? totalPercentageError / count : 0;
12961
+ }
12962
+ /**
12963
+ * Export model with time series-specific data
12964
+ */
12965
+ async export() {
12966
+ const baseExport = await super.export();
12967
+ return {
12968
+ ...baseExport,
12969
+ type: "timeseries",
12970
+ lookback: this.config.modelConfig.lookback
12971
+ };
12972
+ }
12973
+ }
12974
+
12975
+ class NeuralNetworkModel extends BaseModel {
12976
+ constructor(config = {}) {
12977
+ super(config);
12978
+ this.config.modelConfig = {
12979
+ ...this.config.modelConfig,
12980
+ layers: config.modelConfig?.layers || [
12981
+ { units: 64, activation: "relu", dropout: 0.2 },
12982
+ { units: 32, activation: "relu", dropout: 0.1 }
12983
+ ],
12984
+ // Array of hidden layer configurations
12985
+ outputActivation: config.modelConfig?.outputActivation || "linear",
12986
+ // Output layer activation
12987
+ outputUnits: config.modelConfig?.outputUnits || 1,
12988
+ // Number of output units
12989
+ loss: config.modelConfig?.loss || "meanSquaredError",
12990
+ // Loss function
12991
+ metrics: config.modelConfig?.metrics || ["mse", "mae"]
12992
+ // Metrics to track
12993
+ };
12994
+ this._validateLayersConfig();
12995
+ }
12996
+ /**
12997
+ * Validate layers configuration
12998
+ * @private
12999
+ */
13000
+ _validateLayersConfig() {
13001
+ if (!Array.isArray(this.config.modelConfig.layers) || this.config.modelConfig.layers.length === 0) {
13002
+ throw new ModelConfigError(
13003
+ "Neural network must have at least one hidden layer",
13004
+ { model: this.config.name, layers: this.config.modelConfig.layers }
13005
+ );
13006
+ }
13007
+ for (const [index, layer] of this.config.modelConfig.layers.entries()) {
13008
+ if (!layer.units || typeof layer.units !== "number" || layer.units < 1) {
13009
+ throw new ModelConfigError(
13010
+ `Layer ${index} must have a valid "units" property (positive number)`,
13011
+ { model: this.config.name, layer, index }
13012
+ );
13013
+ }
13014
+ if (layer.activation && !this._isValidActivation(layer.activation)) {
13015
+ throw new ModelConfigError(
13016
+ `Layer ${index} has invalid activation function "${layer.activation}"`,
13017
+ { model: this.config.name, layer, index, validActivations: ["relu", "sigmoid", "tanh", "softmax", "elu", "selu"] }
13018
+ );
13019
+ }
13020
+ }
13021
+ }
13022
+ /**
13023
+ * Check if activation function is valid
13024
+ * @private
13025
+ */
13026
+ _isValidActivation(activation) {
13027
+ const validActivations = ["relu", "sigmoid", "tanh", "softmax", "elu", "selu", "linear"];
13028
+ return validActivations.includes(activation);
13029
+ }
13030
+ /**
13031
+ * Build custom neural network architecture
13032
+ */
13033
+ buildModel() {
13034
+ const numFeatures = this.config.features.length;
13035
+ this.model = this.tf.sequential();
13036
+ for (const [index, layerConfig] of this.config.modelConfig.layers.entries()) {
13037
+ const isFirstLayer = index === 0;
13038
+ const layerOptions = {
13039
+ units: layerConfig.units,
13040
+ activation: layerConfig.activation || "relu",
13041
+ useBias: true
13042
+ };
13043
+ if (isFirstLayer) {
13044
+ layerOptions.inputShape = [numFeatures];
13045
+ }
13046
+ this.model.add(this.tf.layers.dense(layerOptions));
13047
+ if (layerConfig.dropout && layerConfig.dropout > 0) {
13048
+ this.model.add(this.tf.layers.dropout({
13049
+ rate: layerConfig.dropout
13050
+ }));
13051
+ }
13052
+ if (layerConfig.batchNormalization) {
13053
+ this.model.add(this.tf.layers.batchNormalization());
13054
+ }
13055
+ }
13056
+ this.model.add(this.tf.layers.dense({
13057
+ units: this.config.modelConfig.outputUnits,
13058
+ activation: this.config.modelConfig.outputActivation
13059
+ }));
13060
+ this.model.compile({
13061
+ optimizer: this.tf.train.adam(this.config.modelConfig.learningRate),
13062
+ loss: this.config.modelConfig.loss,
13063
+ metrics: this.config.modelConfig.metrics
13064
+ });
13065
+ if (this.config.verbose) {
13066
+ console.log(`[MLPlugin] ${this.config.name} - Built custom neural network:`);
13067
+ console.log(` - Hidden layers: ${this.config.modelConfig.layers.length}`);
13068
+ console.log(` - Total parameters:`, this._countParameters());
13069
+ this.model.summary();
13070
+ }
13071
+ }
13072
+ /**
13073
+ * Count total trainable parameters
13074
+ * @private
13075
+ */
13076
+ _countParameters() {
13077
+ if (!this.model) return 0;
13078
+ let totalParams = 0;
13079
+ for (const layer of this.model.layers) {
13080
+ if (layer.countParams) {
13081
+ totalParams += layer.countParams();
13082
+ }
13083
+ }
13084
+ return totalParams;
13085
+ }
13086
+ /**
13087
+ * Add layer to model (before building)
13088
+ * @param {Object} layerConfig - Layer configuration
13089
+ */
13090
+ addLayer(layerConfig) {
13091
+ if (this.model) {
13092
+ throw new ModelConfigError(
13093
+ "Cannot add layer after model is built. Use addLayer() before training.",
13094
+ { model: this.config.name }
13095
+ );
13096
+ }
13097
+ this.config.modelConfig.layers.push(layerConfig);
13098
+ }
13099
+ /**
13100
+ * Set output configuration
13101
+ * @param {Object} outputConfig - Output layer configuration
13102
+ */
13103
+ setOutput(outputConfig) {
13104
+ if (this.model) {
13105
+ throw new ModelConfigError(
13106
+ "Cannot change output after model is built. Use setOutput() before training.",
13107
+ { model: this.config.name }
13108
+ );
13109
+ }
13110
+ if (outputConfig.activation) {
13111
+ this.config.modelConfig.outputActivation = outputConfig.activation;
13112
+ }
13113
+ if (outputConfig.units) {
13114
+ this.config.modelConfig.outputUnits = outputConfig.units;
13115
+ }
13116
+ if (outputConfig.loss) {
13117
+ this.config.modelConfig.loss = outputConfig.loss;
13118
+ }
13119
+ if (outputConfig.metrics) {
13120
+ this.config.modelConfig.metrics = outputConfig.metrics;
13121
+ }
13122
+ }
13123
+ /**
13124
+ * Get model architecture summary
13125
+ */
13126
+ getArchitecture() {
13127
+ return {
13128
+ inputFeatures: this.config.features,
13129
+ hiddenLayers: this.config.modelConfig.layers.map((layer, index) => ({
13130
+ index,
13131
+ units: layer.units,
13132
+ activation: layer.activation || "relu",
13133
+ dropout: layer.dropout || 0,
13134
+ batchNormalization: layer.batchNormalization || false
13135
+ })),
13136
+ outputLayer: {
13137
+ units: this.config.modelConfig.outputUnits,
13138
+ activation: this.config.modelConfig.outputActivation
13139
+ },
13140
+ totalParameters: this._countParameters(),
13141
+ loss: this.config.modelConfig.loss,
13142
+ metrics: this.config.modelConfig.metrics
13143
+ };
13144
+ }
13145
+ /**
13146
+ * Train with early stopping callback
13147
+ * @param {Array} data - Training data
13148
+ * @param {Object} earlyStoppingConfig - Early stopping configuration
13149
+ * @returns {Object} Training results
13150
+ */
13151
+ async trainWithEarlyStopping(data, earlyStoppingConfig = {}) {
13152
+ const {
13153
+ patience = 10,
13154
+ minDelta = 1e-3,
13155
+ monitor = "val_loss",
13156
+ restoreBestWeights = true
13157
+ } = earlyStoppingConfig;
13158
+ const { xs, ys } = this._prepareData(data);
13159
+ if (!this.model) {
13160
+ this.buildModel();
13161
+ }
13162
+ let bestValue = Infinity;
13163
+ let patienceCounter = 0;
13164
+ let bestWeights = null;
13165
+ const callbacks = {
13166
+ onEpochEnd: async (epoch, logs) => {
13167
+ const monitorValue = logs[monitor] || logs.loss;
13168
+ if (this.config.verbose && epoch % 10 === 0) {
13169
+ console.log(`[MLPlugin] ${this.config.name} - Epoch ${epoch}: ${monitor}=${monitorValue.toFixed(4)}`);
13170
+ }
13171
+ if (monitorValue < bestValue - minDelta) {
13172
+ bestValue = monitorValue;
13173
+ patienceCounter = 0;
13174
+ if (restoreBestWeights) {
13175
+ bestWeights = await this.model.getWeights();
13176
+ }
13177
+ } else {
13178
+ patienceCounter++;
13179
+ if (patienceCounter >= patience) {
13180
+ if (this.config.verbose) {
13181
+ console.log(`[MLPlugin] ${this.config.name} - Early stopping at epoch ${epoch}`);
13182
+ }
13183
+ this.model.stopTraining = true;
13184
+ }
13185
+ }
13186
+ }
13187
+ };
13188
+ const history = await this.model.fit(xs, ys, {
13189
+ epochs: this.config.modelConfig.epochs,
13190
+ batchSize: this.config.modelConfig.batchSize,
13191
+ validationSplit: this.config.modelConfig.validationSplit,
13192
+ verbose: this.config.verbose ? 1 : 0,
13193
+ callbacks
13194
+ });
13195
+ if (restoreBestWeights && bestWeights) {
13196
+ this.model.setWeights(bestWeights);
13197
+ }
13198
+ this.isTrained = true;
13199
+ this.stats.trainedAt = (/* @__PURE__ */ new Date()).toISOString();
13200
+ this.stats.samples = data.length;
13201
+ this.stats.loss = history.history.loss[history.history.loss.length - 1];
13202
+ xs.dispose();
13203
+ ys.dispose();
13204
+ return {
13205
+ loss: this.stats.loss,
13206
+ epochs: history.epoch.length,
13207
+ samples: this.stats.samples,
13208
+ stoppedEarly: history.epoch.length < this.config.modelConfig.epochs
13209
+ };
13210
+ }
13211
+ /**
13212
+ * Export model with neural network-specific data
13213
+ */
13214
+ async export() {
13215
+ const baseExport = await super.export();
13216
+ return {
13217
+ ...baseExport,
13218
+ type: "neural-network",
13219
+ architecture: this.getArchitecture()
13220
+ };
13221
+ }
13222
+ }
13223
+
13224
+ class MLPlugin extends Plugin {
13225
+ constructor(options = {}) {
13226
+ super(options);
13227
+ this.config = {
13228
+ models: options.models || {},
13229
+ verbose: options.verbose || false,
13230
+ minTrainingSamples: options.minTrainingSamples || 10
13231
+ };
13232
+ requirePluginDependency("@tensorflow/tfjs-node", "MLPlugin");
13233
+ this.models = {};
13234
+ this.training = /* @__PURE__ */ new Map();
13235
+ this.insertCounters = /* @__PURE__ */ new Map();
13236
+ this.intervals = [];
13237
+ this.stats = {
13238
+ totalTrainings: 0,
13239
+ totalPredictions: 0,
13240
+ totalErrors: 0,
13241
+ startedAt: null
13242
+ };
13243
+ }
13244
+ /**
13245
+ * Install the plugin
13246
+ */
13247
+ async onInstall() {
13248
+ if (this.config.verbose) {
13249
+ console.log("[MLPlugin] Installing ML Plugin...");
13250
+ }
13251
+ for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
13252
+ this._validateModelConfig(modelName, modelConfig);
13253
+ }
13254
+ for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
13255
+ await this._initializeModel(modelName, modelConfig);
13256
+ }
13257
+ for (const [modelName, modelConfig] of Object.entries(this.config.models)) {
13258
+ if (modelConfig.autoTrain) {
13259
+ this._setupAutoTraining(modelName, modelConfig);
13260
+ }
13261
+ }
13262
+ this.stats.startedAt = (/* @__PURE__ */ new Date()).toISOString();
13263
+ if (this.config.verbose) {
13264
+ console.log(`[MLPlugin] Installed with ${Object.keys(this.models).length} models`);
13265
+ }
13266
+ this.emit("installed", {
13267
+ plugin: "MLPlugin",
13268
+ models: Object.keys(this.models)
13269
+ });
13270
+ }
13271
+ /**
13272
+ * Start the plugin
13273
+ */
13274
+ async onStart() {
13275
+ for (const modelName of Object.keys(this.models)) {
13276
+ await this._loadModel(modelName);
13277
+ }
13278
+ if (this.config.verbose) {
13279
+ console.log("[MLPlugin] Started");
13280
+ }
13281
+ }
13282
+ /**
13283
+ * Stop the plugin
13284
+ */
13285
+ async onStop() {
13286
+ for (const handle of this.intervals) {
13287
+ clearInterval(handle);
13288
+ }
13289
+ this.intervals = [];
13290
+ for (const [modelName, model] of Object.entries(this.models)) {
13291
+ if (model && model.dispose) {
13292
+ model.dispose();
13293
+ }
13294
+ }
13295
+ if (this.config.verbose) {
13296
+ console.log("[MLPlugin] Stopped");
13297
+ }
13298
+ }
13299
+ /**
13300
+ * Uninstall the plugin
13301
+ */
13302
+ async onUninstall(options = {}) {
13303
+ await this.onStop();
13304
+ if (options.purgeData) {
13305
+ for (const modelName of Object.keys(this.models)) {
13306
+ await this._deleteModel(modelName);
13307
+ }
13308
+ if (this.config.verbose) {
13309
+ console.log("[MLPlugin] Purged all model data");
13310
+ }
13311
+ }
13312
+ }
13313
+ /**
13314
+ * Validate model configuration
13315
+ * @private
13316
+ */
13317
+ _validateModelConfig(modelName, config) {
13318
+ const validTypes = ["regression", "classification", "timeseries", "neural-network"];
13319
+ if (!config.type || !validTypes.includes(config.type)) {
13320
+ throw new ModelConfigError(
13321
+ `Model "${modelName}" must have a valid type: ${validTypes.join(", ")}`,
13322
+ { modelName, type: config.type, validTypes }
13323
+ );
13324
+ }
13325
+ if (!config.resource) {
13326
+ throw new ModelConfigError(
13327
+ `Model "${modelName}" must specify a resource`,
13328
+ { modelName }
13329
+ );
13330
+ }
13331
+ if (!config.features || !Array.isArray(config.features) || config.features.length === 0) {
13332
+ throw new ModelConfigError(
13333
+ `Model "${modelName}" must specify at least one feature`,
13334
+ { modelName, features: config.features }
13335
+ );
13336
+ }
13337
+ if (!config.target) {
13338
+ throw new ModelConfigError(
13339
+ `Model "${modelName}" must specify a target field`,
13340
+ { modelName }
13341
+ );
13342
+ }
13343
+ }
13344
+ /**
13345
+ * Initialize a model instance
13346
+ * @private
13347
+ */
13348
+ async _initializeModel(modelName, config) {
13349
+ const modelOptions = {
13350
+ name: modelName,
13351
+ resource: config.resource,
13352
+ features: config.features,
13353
+ target: config.target,
13354
+ modelConfig: config.modelConfig || {},
13355
+ verbose: this.config.verbose
13356
+ };
13357
+ try {
13358
+ switch (config.type) {
13359
+ case "regression":
13360
+ this.models[modelName] = new RegressionModel(modelOptions);
13361
+ break;
13362
+ case "classification":
13363
+ this.models[modelName] = new ClassificationModel(modelOptions);
13364
+ break;
13365
+ case "timeseries":
13366
+ this.models[modelName] = new TimeSeriesModel(modelOptions);
13367
+ break;
13368
+ case "neural-network":
13369
+ this.models[modelName] = new NeuralNetworkModel(modelOptions);
13370
+ break;
13371
+ default:
13372
+ throw new ModelConfigError(
13373
+ `Unknown model type: ${config.type}`,
13374
+ { modelName, type: config.type }
13375
+ );
13376
+ }
13377
+ if (this.config.verbose) {
13378
+ console.log(`[MLPlugin] Initialized model "${modelName}" (${config.type})`);
13379
+ }
13380
+ } catch (error) {
13381
+ console.error(`[MLPlugin] Failed to initialize model "${modelName}":`, error.message);
13382
+ throw error;
13383
+ }
13384
+ }
13385
+ /**
13386
+ * Setup auto-training for a model
13387
+ * @private
13388
+ */
13389
+ _setupAutoTraining(modelName, config) {
13390
+ const resource = this.database.resources[config.resource];
13391
+ if (!resource) {
13392
+ console.warn(`[MLPlugin] Resource "${config.resource}" not found for model "${modelName}"`);
13393
+ return;
13394
+ }
13395
+ this.insertCounters.set(modelName, 0);
13396
+ if (config.trainAfterInserts && config.trainAfterInserts > 0) {
13397
+ this.addMiddleware(resource, "insert", async (next, data, options) => {
13398
+ const result = await next(data, options);
13399
+ const currentCount = this.insertCounters.get(modelName) || 0;
13400
+ this.insertCounters.set(modelName, currentCount + 1);
13401
+ if (this.insertCounters.get(modelName) >= config.trainAfterInserts) {
13402
+ if (this.config.verbose) {
13403
+ console.log(`[MLPlugin] Auto-training "${modelName}" after ${config.trainAfterInserts} inserts`);
13404
+ }
13405
+ this.insertCounters.set(modelName, 0);
13406
+ this.train(modelName).catch((err) => {
13407
+ console.error(`[MLPlugin] Auto-training failed for "${modelName}":`, err.message);
13408
+ });
13409
+ }
13410
+ return result;
13411
+ });
13412
+ }
13413
+ if (config.trainInterval && config.trainInterval > 0) {
13414
+ const handle = setInterval(async () => {
13415
+ if (this.config.verbose) {
13416
+ console.log(`[MLPlugin] Auto-training "${modelName}" (interval: ${config.trainInterval}ms)`);
13417
+ }
13418
+ try {
13419
+ await this.train(modelName);
13420
+ } catch (error) {
13421
+ console.error(`[MLPlugin] Auto-training failed for "${modelName}":`, error.message);
13422
+ }
13423
+ }, config.trainInterval);
13424
+ this.intervals.push(handle);
13425
+ if (this.config.verbose) {
13426
+ console.log(`[MLPlugin] Setup interval training for "${modelName}" (every ${config.trainInterval}ms)`);
13427
+ }
13428
+ }
13429
+ }
13430
+ /**
13431
+ * Train a model
13432
+ * @param {string} modelName - Model name
13433
+ * @param {Object} options - Training options
13434
+ * @returns {Object} Training results
13435
+ */
13436
+ async train(modelName, options = {}) {
13437
+ const model = this.models[modelName];
13438
+ if (!model) {
13439
+ throw new ModelNotFoundError(
13440
+ `Model "${modelName}" not found`,
13441
+ { modelName, availableModels: Object.keys(this.models) }
13442
+ );
13443
+ }
13444
+ if (this.training.get(modelName)) {
13445
+ if (this.config.verbose) {
13446
+ console.log(`[MLPlugin] Model "${modelName}" is already training, skipping...`);
13447
+ }
13448
+ return { skipped: true, reason: "already_training" };
13449
+ }
13450
+ this.training.set(modelName, true);
13451
+ try {
13452
+ const modelConfig = this.config.models[modelName];
13453
+ const resource = this.database.resources[modelConfig.resource];
13454
+ if (!resource) {
13455
+ throw new ModelNotFoundError(
13456
+ `Resource "${modelConfig.resource}" not found`,
13457
+ { modelName, resource: modelConfig.resource }
13458
+ );
13459
+ }
13460
+ if (this.config.verbose) {
13461
+ console.log(`[MLPlugin] Fetching training data for "${modelName}"...`);
13462
+ }
13463
+ const [ok, err, data] = await tryFn(() => resource.list());
13464
+ if (!ok) {
13465
+ throw new TrainingError(
13466
+ `Failed to fetch training data: ${err.message}`,
13467
+ { modelName, resource: modelConfig.resource, originalError: err.message }
13468
+ );
13469
+ }
13470
+ if (!data || data.length < this.config.minTrainingSamples) {
13471
+ throw new TrainingError(
13472
+ `Insufficient training data: ${data?.length || 0} samples (minimum: ${this.config.minTrainingSamples})`,
13473
+ { modelName, samples: data?.length || 0, minimum: this.config.minTrainingSamples }
13474
+ );
13475
+ }
13476
+ if (this.config.verbose) {
13477
+ console.log(`[MLPlugin] Training "${modelName}" with ${data.length} samples...`);
13478
+ }
13479
+ const result = await model.train(data);
13480
+ await this._saveModel(modelName);
13481
+ this.stats.totalTrainings++;
13482
+ if (this.config.verbose) {
13483
+ console.log(`[MLPlugin] Training completed for "${modelName}":`, result);
13484
+ }
13485
+ this.emit("modelTrained", {
13486
+ modelName,
13487
+ type: modelConfig.type,
13488
+ result
13489
+ });
13490
+ return result;
13491
+ } catch (error) {
13492
+ this.stats.totalErrors++;
13493
+ if (error instanceof MLError) {
13494
+ throw error;
13495
+ }
13496
+ throw new TrainingError(
13497
+ `Training failed for "${modelName}": ${error.message}`,
13498
+ { modelName, originalError: error.message }
13499
+ );
13500
+ } finally {
13501
+ this.training.set(modelName, false);
13502
+ }
13503
+ }
13504
+ /**
13505
+ * Make a prediction
13506
+ * @param {string} modelName - Model name
13507
+ * @param {Object|Array} input - Input data (object for single prediction, array for time series)
13508
+ * @returns {Object} Prediction result
13509
+ */
13510
+ async predict(modelName, input) {
13511
+ const model = this.models[modelName];
13512
+ if (!model) {
13513
+ throw new ModelNotFoundError(
13514
+ `Model "${modelName}" not found`,
13515
+ { modelName, availableModels: Object.keys(this.models) }
13516
+ );
13517
+ }
13518
+ try {
13519
+ const result = await model.predict(input);
13520
+ this.stats.totalPredictions++;
13521
+ this.emit("prediction", {
13522
+ modelName,
13523
+ input,
13524
+ result
13525
+ });
13526
+ return result;
13527
+ } catch (error) {
13528
+ this.stats.totalErrors++;
13529
+ throw error;
13530
+ }
13531
+ }
13532
+ /**
13533
+ * Make predictions for multiple inputs
13534
+ * @param {string} modelName - Model name
13535
+ * @param {Array} inputs - Array of input objects
13536
+ * @returns {Array} Array of prediction results
13537
+ */
13538
+ async predictBatch(modelName, inputs) {
13539
+ const model = this.models[modelName];
13540
+ if (!model) {
13541
+ throw new ModelNotFoundError(
13542
+ `Model "${modelName}" not found`,
13543
+ { modelName, availableModels: Object.keys(this.models) }
13544
+ );
13545
+ }
13546
+ return await model.predictBatch(inputs);
13547
+ }
13548
+ /**
13549
+ * Retrain a model (reset and train from scratch)
13550
+ * @param {string} modelName - Model name
13551
+ * @param {Object} options - Options
13552
+ * @returns {Object} Training results
13553
+ */
13554
+ async retrain(modelName, options = {}) {
13555
+ const model = this.models[modelName];
13556
+ if (!model) {
13557
+ throw new ModelNotFoundError(
13558
+ `Model "${modelName}" not found`,
13559
+ { modelName, availableModels: Object.keys(this.models) }
13560
+ );
13561
+ }
13562
+ if (model.dispose) {
13563
+ model.dispose();
13564
+ }
13565
+ const modelConfig = this.config.models[modelName];
13566
+ await this._initializeModel(modelName, modelConfig);
13567
+ return await this.train(modelName, options);
13568
+ }
13569
+ /**
13570
+ * Get model statistics
13571
+ * @param {string} modelName - Model name
13572
+ * @returns {Object} Model stats
13573
+ */
13574
+ getModelStats(modelName) {
13575
+ const model = this.models[modelName];
13576
+ if (!model) {
13577
+ throw new ModelNotFoundError(
13578
+ `Model "${modelName}" not found`,
13579
+ { modelName, availableModels: Object.keys(this.models) }
13580
+ );
13581
+ }
13582
+ return model.getStats();
13583
+ }
13584
+ /**
13585
+ * Get plugin statistics
13586
+ * @returns {Object} Plugin stats
13587
+ */
13588
+ getStats() {
13589
+ return {
13590
+ ...this.stats,
13591
+ models: Object.keys(this.models).length,
13592
+ trainedModels: Object.values(this.models).filter((m) => m.isTrained).length
13593
+ };
13594
+ }
13595
+ /**
13596
+ * Export a model
13597
+ * @param {string} modelName - Model name
13598
+ * @returns {Object} Serialized model
13599
+ */
13600
+ async exportModel(modelName) {
13601
+ const model = this.models[modelName];
13602
+ if (!model) {
13603
+ throw new ModelNotFoundError(
13604
+ `Model "${modelName}" not found`,
13605
+ { modelName, availableModels: Object.keys(this.models) }
13606
+ );
13607
+ }
13608
+ return await model.export();
13609
+ }
13610
+ /**
13611
+ * Import a model
13612
+ * @param {string} modelName - Model name
13613
+ * @param {Object} data - Serialized model data
13614
+ */
13615
+ async importModel(modelName, data) {
13616
+ const model = this.models[modelName];
13617
+ if (!model) {
13618
+ throw new ModelNotFoundError(
13619
+ `Model "${modelName}" not found`,
13620
+ { modelName, availableModels: Object.keys(this.models) }
13621
+ );
13622
+ }
13623
+ await model.import(data);
13624
+ await this._saveModel(modelName);
13625
+ if (this.config.verbose) {
13626
+ console.log(`[MLPlugin] Imported model "${modelName}"`);
13627
+ }
13628
+ }
13629
+ /**
13630
+ * Save model to plugin storage
13631
+ * @private
13632
+ */
13633
+ async _saveModel(modelName) {
13634
+ try {
13635
+ const storage = this.getStorage();
13636
+ const exportedModel = await this.models[modelName].export();
13637
+ if (!exportedModel) {
13638
+ if (this.config.verbose) {
13639
+ console.log(`[MLPlugin] Model "${modelName}" not trained, skipping save`);
13640
+ }
13641
+ return;
13642
+ }
13643
+ await storage.patch(`model_${modelName}`, {
13644
+ modelName,
13645
+ data: JSON.stringify(exportedModel),
13646
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
13647
+ });
13648
+ if (this.config.verbose) {
13649
+ console.log(`[MLPlugin] Saved model "${modelName}" to plugin storage`);
13650
+ }
13651
+ } catch (error) {
13652
+ console.error(`[MLPlugin] Failed to save model "${modelName}":`, error.message);
13653
+ }
13654
+ }
13655
+ /**
13656
+ * Load model from plugin storage
13657
+ * @private
13658
+ */
13659
+ async _loadModel(modelName) {
13660
+ try {
13661
+ const storage = this.getStorage();
13662
+ const [ok, err, record] = await tryFn(() => storage.get(`model_${modelName}`));
13663
+ if (!ok || !record) {
13664
+ if (this.config.verbose) {
13665
+ console.log(`[MLPlugin] No saved model found for "${modelName}"`);
13666
+ }
13667
+ return;
13668
+ }
13669
+ const modelData = JSON.parse(record.data);
13670
+ await this.models[modelName].import(modelData);
13671
+ if (this.config.verbose) {
13672
+ console.log(`[MLPlugin] Loaded model "${modelName}" from plugin storage`);
13673
+ }
13674
+ } catch (error) {
13675
+ console.error(`[MLPlugin] Failed to load model "${modelName}":`, error.message);
13676
+ }
13677
+ }
13678
+ /**
13679
+ * Delete model from plugin storage
13680
+ * @private
13681
+ */
13682
+ async _deleteModel(modelName) {
13683
+ try {
13684
+ const storage = this.getStorage();
13685
+ await storage.delete(`model_${modelName}`);
13686
+ if (this.config.verbose) {
13687
+ console.log(`[MLPlugin] Deleted model "${modelName}" from plugin storage`);
13688
+ }
13689
+ } catch (error) {
13690
+ if (this.config.verbose) {
13691
+ console.log(`[MLPlugin] Could not delete model "${modelName}": ${error.message}`);
13692
+ }
13693
+ }
13694
+ }
13695
+ }
13696
+
11929
13697
  class SqsConsumer {
11930
13698
  constructor({ queueUrl, onMessage, onError, poolingInterval = 5e3, maxMessages = 10, region = "us-east-1", credentials, endpoint, driver = "sqs" }) {
11931
13699
  this.driver = driver;
@@ -18457,6 +20225,7 @@ ${errorDetails}`,
18457
20225
  events = {},
18458
20226
  asyncEvents = true,
18459
20227
  asyncPartitions = true,
20228
+ strictPartitions = false,
18460
20229
  createdBy = "user"
18461
20230
  } = config;
18462
20231
  this.name = name;
@@ -18488,6 +20257,7 @@ ${errorDetails}`,
18488
20257
  allNestedObjectsOptional,
18489
20258
  asyncEvents,
18490
20259
  asyncPartitions,
20260
+ strictPartitions,
18491
20261
  createdBy
18492
20262
  };
18493
20263
  this.hooks = {
@@ -19240,17 +21010,31 @@ ${errorDetails}`,
19240
21010
  throw errPut;
19241
21011
  }
19242
21012
  const insertedObject = await this.get(finalId);
19243
- if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
19244
- setImmediate(() => {
19245
- this.createPartitionReferences(insertedObject).catch((err) => {
21013
+ if (this.config.partitions && Object.keys(this.config.partitions).length > 0) {
21014
+ if (this.config.strictPartitions) {
21015
+ await this.createPartitionReferences(insertedObject);
21016
+ } else if (this.config.asyncPartitions) {
21017
+ setImmediate(() => {
21018
+ this.createPartitionReferences(insertedObject).catch((err) => {
21019
+ this.emit("partitionIndexError", {
21020
+ operation: "insert",
21021
+ id: finalId,
21022
+ error: err,
21023
+ message: err.message
21024
+ });
21025
+ });
21026
+ });
21027
+ } else {
21028
+ const [ok, err] = await tryFn(() => this.createPartitionReferences(insertedObject));
21029
+ if (!ok) {
19246
21030
  this.emit("partitionIndexError", {
19247
21031
  operation: "insert",
19248
21032
  id: finalId,
19249
21033
  error: err,
19250
21034
  message: err.message
19251
21035
  });
19252
- });
19253
- });
21036
+ }
21037
+ }
19254
21038
  const nonPartitionHooks = this.hooks.afterInsert.filter(
19255
21039
  (hook) => !hook.toString().includes("createPartitionReferences")
19256
21040
  );
@@ -19545,17 +21329,31 @@ ${errorDetails}`,
19545
21329
  body: finalBody,
19546
21330
  behavior: this.behavior
19547
21331
  });
19548
- if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
19549
- setImmediate(() => {
19550
- this.handlePartitionReferenceUpdates(originalData, updatedData).catch((err2) => {
21332
+ if (this.config.partitions && Object.keys(this.config.partitions).length > 0) {
21333
+ if (this.config.strictPartitions) {
21334
+ await this.handlePartitionReferenceUpdates(originalData, updatedData);
21335
+ } else if (this.config.asyncPartitions) {
21336
+ setImmediate(() => {
21337
+ this.handlePartitionReferenceUpdates(originalData, updatedData).catch((err2) => {
21338
+ this.emit("partitionIndexError", {
21339
+ operation: "update",
21340
+ id,
21341
+ error: err2,
21342
+ message: err2.message
21343
+ });
21344
+ });
21345
+ });
21346
+ } else {
21347
+ const [ok2, err2] = await tryFn(() => this.handlePartitionReferenceUpdates(originalData, updatedData));
21348
+ if (!ok2) {
19551
21349
  this.emit("partitionIndexError", {
19552
21350
  operation: "update",
19553
21351
  id,
19554
21352
  error: err2,
19555
21353
  message: err2.message
19556
21354
  });
19557
- });
19558
- });
21355
+ }
21356
+ }
19559
21357
  const nonPartitionHooks = this.hooks.afterUpdate.filter(
19560
21358
  (hook) => !hook.toString().includes("handlePartitionReferenceUpdates")
19561
21359
  );
@@ -19668,7 +21466,9 @@ ${errorDetails}`,
19668
21466
  if (this.config.partitions && Object.keys(this.config.partitions).length > 0) {
19669
21467
  const oldData = { ...currentData, id };
19670
21468
  const newData = { ...mergedData, id };
19671
- if (this.config.asyncPartitions) {
21469
+ if (this.config.strictPartitions) {
21470
+ await this.handlePartitionReferenceUpdates(oldData, newData);
21471
+ } else if (this.config.asyncPartitions) {
19672
21472
  setImmediate(() => {
19673
21473
  this.handlePartitionReferenceUpdates(oldData, newData).catch((err) => {
19674
21474
  this.emit("partitionIndexError", {
@@ -19798,7 +21598,9 @@ ${errorDetails}`,
19798
21598
  }
19799
21599
  const replacedObject = { id, ...validatedAttributes };
19800
21600
  if (this.config.partitions && Object.keys(this.config.partitions).length > 0) {
19801
- if (this.config.asyncPartitions) {
21601
+ if (this.config.strictPartitions) {
21602
+ await this.handlePartitionReferenceUpdates({}, replacedObject);
21603
+ } else if (this.config.asyncPartitions) {
19802
21604
  setImmediate(() => {
19803
21605
  this.handlePartitionReferenceUpdates({}, replacedObject).catch((err) => {
19804
21606
  this.emit("partitionIndexError", {
@@ -19938,17 +21740,31 @@ ${errorDetails}`,
19938
21740
  });
19939
21741
  const oldData = { ...originalData, id };
19940
21742
  const newData = { ...validatedAttributes, id };
19941
- if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
19942
- setImmediate(() => {
19943
- this.handlePartitionReferenceUpdates(oldData, newData).catch((err2) => {
21743
+ if (this.config.partitions && Object.keys(this.config.partitions).length > 0) {
21744
+ if (this.config.strictPartitions) {
21745
+ await this.handlePartitionReferenceUpdates(oldData, newData);
21746
+ } else if (this.config.asyncPartitions) {
21747
+ setImmediate(() => {
21748
+ this.handlePartitionReferenceUpdates(oldData, newData).catch((err2) => {
21749
+ this.emit("partitionIndexError", {
21750
+ operation: "updateConditional",
21751
+ id,
21752
+ error: err2,
21753
+ message: err2.message
21754
+ });
21755
+ });
21756
+ });
21757
+ } else {
21758
+ const [ok2, err2] = await tryFn(() => this.handlePartitionReferenceUpdates(oldData, newData));
21759
+ if (!ok2) {
19944
21760
  this.emit("partitionIndexError", {
19945
21761
  operation: "updateConditional",
19946
21762
  id,
19947
21763
  error: err2,
19948
21764
  message: err2.message
19949
21765
  });
19950
- });
19951
- });
21766
+ }
21767
+ }
19952
21768
  const nonPartitionHooks = this.hooks.afterUpdate.filter(
19953
21769
  (hook) => !hook.toString().includes("handlePartitionReferenceUpdates")
19954
21770
  );
@@ -20024,17 +21840,31 @@ ${errorDetails}`,
20024
21840
  operation: "delete",
20025
21841
  id
20026
21842
  });
20027
- if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
20028
- setImmediate(() => {
20029
- this.deletePartitionReferences(objectData).catch((err3) => {
21843
+ if (this.config.partitions && Object.keys(this.config.partitions).length > 0 && objectData) {
21844
+ if (this.config.strictPartitions) {
21845
+ await this.deletePartitionReferences(objectData);
21846
+ } else if (this.config.asyncPartitions) {
21847
+ setImmediate(() => {
21848
+ this.deletePartitionReferences(objectData).catch((err3) => {
21849
+ this.emit("partitionIndexError", {
21850
+ operation: "delete",
21851
+ id,
21852
+ error: err3,
21853
+ message: err3.message
21854
+ });
21855
+ });
21856
+ });
21857
+ } else {
21858
+ const [ok3, err3] = await tryFn(() => this.deletePartitionReferences(objectData));
21859
+ if (!ok3) {
20030
21860
  this.emit("partitionIndexError", {
20031
21861
  operation: "delete",
20032
21862
  id,
20033
21863
  error: err3,
20034
21864
  message: err3.message
20035
21865
  });
20036
- });
20037
- });
21866
+ }
21867
+ }
20038
21868
  const nonPartitionHooks = this.hooks.afterDelete.filter(
20039
21869
  (hook) => !hook.toString().includes("deletePartitionReferences")
20040
21870
  );
@@ -21405,10 +23235,13 @@ function validateResourceConfig(config) {
21405
23235
  class Database extends EventEmitter {
21406
23236
  constructor(options) {
21407
23237
  super();
21408
- this.id = idGenerator(7);
23238
+ this.id = (() => {
23239
+ const [ok, err, id] = tryFn(() => idGenerator(7));
23240
+ return ok && id ? id : `db-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
23241
+ })();
21409
23242
  this.version = "1";
21410
23243
  this.s3dbVersion = (() => {
21411
- const [ok, err, version] = tryFn(() => true ? "12.4.0" : "latest");
23244
+ const [ok, err, version] = tryFn(() => true ? "13.0.0" : "latest");
21412
23245
  return ok ? version : "latest";
21413
23246
  })();
21414
23247
  this._resourcesMap = {};
@@ -21442,6 +23275,7 @@ class Database extends EventEmitter {
21442
23275
  this.versioningEnabled = options.versioningEnabled || false;
21443
23276
  this.persistHooks = options.persistHooks || false;
21444
23277
  this.strictValidation = options.strictValidation !== false;
23278
+ this.strictHooks = options.strictHooks || false;
21445
23279
  this._initHooks();
21446
23280
  let connectionString = options.connectionString;
21447
23281
  if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
@@ -21472,18 +23306,25 @@ class Database extends EventEmitter {
21472
23306
  this.connectionString = connectionString;
21473
23307
  this.bucket = this.client.bucket;
21474
23308
  this.keyPrefix = this.client.keyPrefix;
21475
- if (!this._exitListenerRegistered) {
23309
+ this._registerExitListener();
23310
+ }
23311
+ /**
23312
+ * Register process exit listener for automatic cleanup
23313
+ * @private
23314
+ */
23315
+ _registerExitListener() {
23316
+ if (!this._exitListenerRegistered && typeof process !== "undefined") {
21476
23317
  this._exitListenerRegistered = true;
21477
- if (typeof process !== "undefined") {
21478
- process.on("exit", async () => {
21479
- if (this.isConnected()) {
21480
- await tryFn(() => this.disconnect());
21481
- }
21482
- });
21483
- }
23318
+ this._exitListener = async () => {
23319
+ if (this.isConnected()) {
23320
+ await tryFn(() => this.disconnect());
23321
+ }
23322
+ };
23323
+ process.on("exit", this._exitListener);
21484
23324
  }
21485
23325
  }
21486
23326
  async connect() {
23327
+ this._registerExitListener();
21487
23328
  await this.startPlugins();
21488
23329
  let metadata = null;
21489
23330
  let needsHealing = false;
@@ -22446,11 +24287,16 @@ class Database extends EventEmitter {
22446
24287
  if (this.client && typeof this.client.removeAllListeners === "function") {
22447
24288
  this.client.removeAllListeners();
22448
24289
  }
24290
+ await this.emit("disconnected", /* @__PURE__ */ new Date());
22449
24291
  this.removeAllListeners();
24292
+ if (this._exitListener && typeof process !== "undefined") {
24293
+ process.off("exit", this._exitListener);
24294
+ this._exitListener = null;
24295
+ this._exitListenerRegistered = false;
24296
+ }
22450
24297
  this.savedMetadata = null;
22451
24298
  this.plugins = {};
22452
24299
  this.pluginList = [];
22453
- this.emit("disconnected", /* @__PURE__ */ new Date());
22454
24300
  });
22455
24301
  }
22456
24302
  /**
@@ -22554,6 +24400,13 @@ class Database extends EventEmitter {
22554
24400
  const [ok, error] = await tryFn(() => hook({ database: this, ...context }));
22555
24401
  if (!ok) {
22556
24402
  this.emit("hookError", { event, error, context });
24403
+ if (this.strictHooks) {
24404
+ throw new DatabaseError(`Hook execution failed for event '${event}': ${error.message}`, {
24405
+ event,
24406
+ originalError: error,
24407
+ context
24408
+ });
24409
+ }
22557
24410
  }
22558
24411
  }
22559
24412
  }
@@ -38881,30 +40734,42 @@ class MemoryClient extends EventEmitter {
38881
40734
  const resourceStats = {};
38882
40735
  for (const [resourceName, keys] of resourceMap.entries()) {
38883
40736
  const records = [];
40737
+ const resource = database && database.resources && database.resources[resourceName];
38884
40738
  for (const key of keys) {
38885
- const obj = await this.getObject(key);
38886
40739
  const idMatch = key.match(/\/id=([^/]+)/);
38887
40740
  const recordId = idMatch ? idMatch[1] : null;
38888
- const record = { ...obj.Metadata };
38889
- if (recordId && !record.id) {
38890
- record.id = recordId;
38891
- }
38892
- if (obj.Body) {
38893
- const chunks = [];
38894
- for await (const chunk2 of obj.Body) {
38895
- chunks.push(chunk2);
40741
+ let record;
40742
+ if (resource && recordId) {
40743
+ try {
40744
+ record = await resource.get(recordId);
40745
+ } catch (err) {
40746
+ console.warn(`Failed to get record ${recordId} from resource ${resourceName}, using fallback`);
40747
+ record = null;
38896
40748
  }
38897
- const bodyBuffer = Buffer.concat(chunks);
38898
- const bodyStr = bodyBuffer.toString("utf-8");
38899
- if (bodyStr.startsWith("{") || bodyStr.startsWith("[")) {
38900
- try {
38901
- const bodyData = JSON.parse(bodyStr);
38902
- Object.assign(record, bodyData);
38903
- } catch {
40749
+ }
40750
+ if (!record) {
40751
+ const obj = await this.getObject(key);
40752
+ record = { ...obj.Metadata };
40753
+ if (recordId && !record.id) {
40754
+ record.id = recordId;
40755
+ }
40756
+ if (obj.Body) {
40757
+ const chunks = [];
40758
+ for await (const chunk2 of obj.Body) {
40759
+ chunks.push(chunk2);
40760
+ }
40761
+ const bodyBuffer = Buffer.concat(chunks);
40762
+ const bodyStr = bodyBuffer.toString("utf-8");
40763
+ if (bodyStr.startsWith("{") || bodyStr.startsWith("[")) {
40764
+ try {
40765
+ const bodyData = JSON.parse(bodyStr);
40766
+ Object.assign(record, bodyData);
40767
+ } catch {
40768
+ record._body = bodyStr;
40769
+ }
40770
+ } else if (bodyStr) {
38904
40771
  record._body = bodyStr;
38905
40772
  }
38906
- } else if (bodyStr) {
38907
- record._body = bodyStr;
38908
40773
  }
38909
40774
  }
38910
40775
  records.push(record);
@@ -39998,6 +41863,7 @@ exports.FilesystemCache = FilesystemCache;
39998
41863
  exports.FullTextPlugin = FullTextPlugin;
39999
41864
  exports.GeoPlugin = GeoPlugin;
40000
41865
  exports.InvalidResourceItem = InvalidResourceItem;
41866
+ exports.MLPlugin = MLPlugin;
40001
41867
  exports.MemoryCache = MemoryCache;
40002
41868
  exports.MemoryClient = MemoryClient;
40003
41869
  exports.MemoryStorage = MemoryStorage;